From 3b88d45841a26cd584a44cee8ed54ecb6a4687c6 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Tue, 27 Oct 2020 14:30:21 -0400 Subject: [PATCH 01/10] Add placeholder files for UIViewRepresentable replacement of TextEditor --- .../project.pbxproj | 8 ++++++++ iOS/PostEditor/PostBodyTextView.swift | 20 +++++++++++++++++++ iOS/PostEditor/PostTitleTextView.swift | 20 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 iOS/PostEditor/PostBodyTextView.swift create mode 100644 iOS/PostEditor/PostTitleTextView.swift diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index 4bc5d99..8e4d929 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -53,6 +53,8 @@ 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 */; }; 17B3E965250FAA9000EE9748 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */; }; 17B5103B2515448D00E9631F /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 17B5103A2515448D00E9631F /* Credits.rtf */; }; 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 = ""; }; 17A5389124DDED0000DEFF9A /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; 17A67CAE251A5DD7002F163D /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = ""; }; + 17AD0A5D25489E810057D763 /* PostTitleTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTitleTextView.swift; sourceTree = ""; }; + 17AD0A6325489E900057D763 /* PostBodyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBodyTextView.swift; sourceTree = ""; }; 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 17B5103A2515448D00E9631F /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; }; @@ -294,6 +298,8 @@ 1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */, 173E19D0254318F600440F0F /* RemoteChangePromptView.swift */, 173E19E2254329CC00440F0F /* PostTextEditingView.swift */, + 17AD0A5D25489E810057D763 /* PostTitleTextView.swift */, + 17AD0A6325489E900057D763 /* PostBodyTextView.swift */, ); path = PostEditor; sourceTree = ""; @@ -694,6 +700,8 @@ 170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */, 17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */, 17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */, + 17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */, + 17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */, 17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */, 171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */, 1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */, diff --git a/iOS/PostEditor/PostBodyTextView.swift b/iOS/PostEditor/PostBodyTextView.swift new file mode 100644 index 0000000..0ca4964 --- /dev/null +++ b/iOS/PostEditor/PostBodyTextView.swift @@ -0,0 +1,20 @@ +// +// PostBodyTextView.swift +// WriteFreely-MultiPlatform (iOS) +// +// Created by Angelo Stavrow on 2020-10-27. +// + +import SwiftUI + +struct PostBodyTextView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct PostBodyTextView_Previews: PreviewProvider { + static var previews: some View { + PostBodyTextView() + } +} diff --git a/iOS/PostEditor/PostTitleTextView.swift b/iOS/PostEditor/PostTitleTextView.swift new file mode 100644 index 0000000..f36a2bb --- /dev/null +++ b/iOS/PostEditor/PostTitleTextView.swift @@ -0,0 +1,20 @@ +// +// PostTitleTextView.swift +// WriteFreely-MultiPlatform (iOS) +// +// Created by Angelo Stavrow on 2020-10-27. +// + +import SwiftUI + +struct PostTitleTextView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct PostTitleTextView_Previews: PreviewProvider { + static var previews: some View { + PostTitleTextView() + } +} From aa519354825c9f069eaa4ec2498aa0d674101a5f Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Tue, 27 Oct 2020 14:37:35 -0400 Subject: [PATCH 02/10] Create UIViewRepresentable / UITextView for iOS post body editor --- iOS/PostEditor/PostBodyTextView.swift | 62 +++++++++++++++++++++------ 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/iOS/PostEditor/PostBodyTextView.swift b/iOS/PostEditor/PostBodyTextView.swift index 0ca4964..ba7713c 100644 --- a/iOS/PostEditor/PostBodyTextView.swift +++ b/iOS/PostEditor/PostBodyTextView.swift @@ -1,20 +1,54 @@ -// -// PostBodyTextView.swift -// WriteFreely-MultiPlatform (iOS) -// -// Created by Angelo Stavrow on 2020-10-27. -// +// Based on https://stackoverflow.com/a/56508132/1234545 import SwiftUI -struct PostBodyTextView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} +struct PostBodyTextView: UIViewRepresentable { -struct PostBodyTextView_Previews: PreviewProvider { - static var previews: some View { - PostBodyTextView() + class Coordinator: NSObject, UITextViewDelegate { + @Binding var text: String + @Binding var isFirstResponder: Bool + var didBecomeFirstResponder: Bool = false + + init(text: Binding, isFirstResponder: Binding) { + _text = text + _isFirstResponder = isFirstResponder + } + + func textViewDidChangeSelection(_ textView: UITextView) { + DispatchQueue.main.async { + self.text = textView.text ?? "" + } + } + } + + @Binding var text: String + @Binding var textStyle: UIFont + @Binding var isFirstResponder: Bool + + func makeUIView(context: UIViewRepresentableContext) -> UITextView { + let textView = UITextView(frame: .zero) + textView.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() -> PostBodyTextView.Coordinator { + return Coordinator(text: $text, isFirstResponder: $isFirstResponder) + } + + func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { + 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 + } } } From 86cf0e976de846a69416b2b0b71b716cc4ad2679 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Tue, 27 Oct 2020 14:39:27 -0400 Subject: [PATCH 03/10] Create UIViewRepresentable / UITextView for iOS post title editor --- iOS/PostEditor/PostTitleTextView.swift | 99 ++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/iOS/PostEditor/PostTitleTextView.swift b/iOS/PostEditor/PostTitleTextView.swift index f36a2bb..c29f843 100644 --- a/iOS/PostEditor/PostTitleTextView.swift +++ b/iOS/PostEditor/PostTitleTextView.swift @@ -1,20 +1,95 @@ -// -// PostTitleTextView.swift -// WriteFreely-MultiPlatform (iOS) -// -// Created by Angelo Stavrow on 2020-10-27. -// +// Based on https://lostmoa.com/blog/DynamicHeightForTextFieldInSwiftUI/ +// and https://stackoverflow.com/a/56508132/1234545 import SwiftUI -struct PostTitleTextView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) +class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate { + @Binding var text: String + @Binding var isFirstResponder: Bool + var didBecomeFirstResponder: Bool = false + var postTitleTextView: PostTitleTextView + + weak var textView: UITextView? + + init(_ textView: PostTitleTextView, text: Binding, isFirstResponder: Binding) { + self.postTitleTextView = textView + _text = text + _isFirstResponder = isFirstResponder + } + + func textViewDidChangeSelection(_ 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() + 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 + } + } } } -struct PostTitleTextView_Previews: PreviewProvider { - static var previews: some View { - PostTitleTextView() +struct PostTitleTextView: UIViewRepresentable { + @Binding var text: String + @Binding var textStyle: UIFont + @Binding var height: CGFloat + @Binding var isFirstResponder: Bool + + func makeUIView(context: UIViewRepresentableContext) -> 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() -> Coordinator { + return Coordinator(self, text: $text, isFirstResponder: $isFirstResponder) + } + + func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { + 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 + } } } From 3becfcbf7325c4c5fa03c394701f47abe9bb81df Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Tue, 27 Oct 2020 16:02:28 -0400 Subject: [PATCH 04/10] Use PostBodyTitleView and PostBodyTextView for post editor --- Shared/PostEditor/PostEditorModel.swift | 4 +- iOS/PostEditor/PostBodyTextView.swift | 12 +++- iOS/PostEditor/PostTextEditingView.swift | 85 +++++++++++++++++------- iOS/PostEditor/PostTitleTextView.swift | 2 +- 4 files changed, 74 insertions(+), 29 deletions(-) diff --git a/Shared/PostEditor/PostEditorModel.swift b/Shared/PostEditor/PostEditorModel.swift index ca8959f..84e774c 100644 --- a/Shared/PostEditor/PostEditorModel.swift +++ b/Shared/PostEditor/PostEditorModel.swift @@ -3,8 +3,8 @@ import CoreData enum PostAppearance: String { case sans = "OpenSans-Regular" - case mono = "Hack" - case serif = "Lora" + case mono = "Hack-Regular" + case serif = "Lora-Regular" } struct PostEditorModel { diff --git a/iOS/PostEditor/PostBodyTextView.swift b/iOS/PostEditor/PostBodyTextView.swift index ba7713c..2de4636 100644 --- a/iOS/PostEditor/PostBodyTextView.swift +++ b/iOS/PostEditor/PostBodyTextView.swift @@ -24,6 +24,7 @@ struct PostBodyTextView: UIViewRepresentable { @Binding var text: String @Binding var textStyle: UIFont @Binding var isFirstResponder: Bool + var lineSpacing: CGFloat func makeUIView(context: UIViewRepresentableContext) -> UITextView { let textView = UITextView(frame: .zero) @@ -40,7 +41,16 @@ struct PostBodyTextView: UIViewRepresentable { } func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { - uiView.text = text + let attributedString = NSMutableAttributedString(string: text) + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = lineSpacing + attributedString.addAttribute( + NSAttributedString.Key.paragraphStyle, + value: paragraphStyle, + range: NSMakeRange(0, attributedString.length) // swiftlint:disable:this legacy_constructor + ) + + uiView.attributedText = attributedString let font = textStyle let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle) uiView.font = fontMetrics.scaledFont(for: font) diff --git a/iOS/PostEditor/PostTextEditingView.swift b/iOS/PostEditor/PostTextEditingView.swift index c24530e..0f85e84 100644 --- a/iOS/PostEditor/PostTextEditingView.swift +++ b/iOS/PostEditor/PostTextEditingView.swift @@ -6,6 +6,11 @@ struct PostTextEditingView: View { @Binding var updatingTitleFromServer: Bool @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 titleIsFirstResponder: Bool = true + @State private var bodyTextStyle: UIFont = UIFont(name: "Lora-Regular", size: 17)! + @State private var bodyIsFirstResponder: Bool = false private let bodyLineSpacingMultiplier: CGFloat = 0.5 init( @@ -19,41 +24,69 @@ struct PostTextEditingView: View { UITextView.appearance().backgroundColor = .clear } + var titleFieldHeight: CGFloat { + let minHeight: CGFloat = 50 + if titleTextHeight < minHeight { + return minHeight + } + return titleTextHeight + } + var body: some View { 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) { - if post.body.count == 0 { - Text("Write…") - .font(.custom(appearance.rawValue, size: 17, relativeTo: .body)) + if post.title.count == 0 { + Text("Title (optional)") + .font(Font(titleTextStyle)) .foregroundColor(Color(UIColor.placeholderText)) .padding(.horizontal, 4) .padding(.vertical, 8) } - TextEditor(text: $post.body) - .font(.custom(appearance.rawValue, size: 17, relativeTo: .body)) - .lineSpacing( - 17 * ( - horizontalSizeClass == .compact ? bodyLineSpacingMultiplier / 2 : bodyLineSpacingMultiplier - ) - ) - .onChange(of: post.body) { _ in - if post.status == PostStatus.published.rawValue && !updatingBodyFromServer { - post.status = PostStatus.edited.rawValue - } - if updatingBodyFromServer { - updatingBodyFromServer = false - } + PostTitleTextView( + text: $post.title, + textStyle: $titleTextStyle, + height: $titleTextHeight, + isFirstResponder: $titleIsFirstResponder + ) + .frame(height: titleFieldHeight) + .onChange(of: post.title) { _ in + if post.status == PostStatus.published.rawValue && !updatingTitleFromServer { + post.status = PostStatus.edited.rawValue } + 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, + lineSpacing: 17 * ( + horizontalSizeClass == .compact ? bodyLineSpacingMultiplier / 2 : bodyLineSpacingMultiplier + ) + ) + .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: { switch post.appearance { case "sans": @@ -63,6 +96,8 @@ struct PostTextEditingView: View { default: self.appearance = .serif } + self.titleTextStyle = UIFont(name: appearance.rawValue, size: 26)! + self.bodyTextStyle = UIFont(name: appearance.rawValue, size: 17)! }) } } diff --git a/iOS/PostEditor/PostTitleTextView.swift b/iOS/PostEditor/PostTitleTextView.swift index c29f843..434722c 100644 --- a/iOS/PostEditor/PostTitleTextView.swift +++ b/iOS/PostEditor/PostTitleTextView.swift @@ -24,7 +24,7 @@ class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate { } func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if (text == "\n") { + if text == "\n" { self.isFirstResponder.toggle() return false } From 61cc9464c0b07db142bf653d116f00e4c9e61c36 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Wed, 28 Oct 2020 10:36:08 -0400 Subject: [PATCH 05/10] Fix insertion-point bug in body editor --- iOS/PostEditor/PostBodyTextView.swift | 75 ++++++++++++++++-------- iOS/PostEditor/PostTextEditingView.swift | 6 +- iOS/PostEditor/PostTitleTextView.swift | 6 +- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/iOS/PostEditor/PostBodyTextView.swift b/iOS/PostEditor/PostBodyTextView.swift index 2de4636..2eaef56 100644 --- a/iOS/PostEditor/PostBodyTextView.swift +++ b/iOS/PostEditor/PostBodyTextView.swift @@ -1,26 +1,44 @@ -// Based on https://stackoverflow.com/a/56508132/1234545 +// Based on https://stackoverflow.com/a/56508132/1234545 and https://stackoverflow.com/a/48360549/1234545 import SwiftUI -struct PostBodyTextView: UIViewRepresentable { +class PostBodyCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate { + @Binding var text: String + @Binding var isFirstResponder: Bool + var lineSpacingMultiplier: CGFloat + var didBecomeFirstResponder: Bool = false + var postBodyTextView: PostBodyTextView - class Coordinator: NSObject, UITextViewDelegate { - @Binding var text: String - @Binding var isFirstResponder: Bool - var didBecomeFirstResponder: Bool = false + weak var textView: UITextView? - init(text: Binding, isFirstResponder: Binding) { - _text = text - _isFirstResponder = isFirstResponder - } + init( + _ textView: PostBodyTextView, + text: Binding, + isFirstResponder: Binding, + lineSpacingMultiplier: CGFloat + ) { + self.postBodyTextView = textView + _text = text + _isFirstResponder = isFirstResponder + self.lineSpacingMultiplier = lineSpacingMultiplier + } - func textViewDidChangeSelection(_ textView: UITextView) { - DispatchQueue.main.async { - self.text = textView.text ?? "" - } + func textViewDidChange(_ textView: UITextView) { + DispatchQueue.main.async { + self.postBodyTextView.text = textView.text ?? "" } } + func layoutManager( + _ layoutManager: NSLayoutManager, + lineSpacingAfterGlyphAt glyphIndex: Int, + withProposedLineFragmentRect rect: CGRect + ) -> CGFloat { + return 17 * lineSpacingMultiplier + } +} + +struct PostBodyTextView: UIViewRepresentable { @Binding var text: String @Binding var textStyle: UIFont @Binding var isFirstResponder: Bool @@ -28,29 +46,36 @@ struct PostBodyTextView: UIViewRepresentable { func makeUIView(context: UIViewRepresentableContext) -> 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() -> PostBodyTextView.Coordinator { - return Coordinator(text: $text, isFirstResponder: $isFirstResponder) + func makeCoordinator() -> PostBodyCoordinator { + return Coordinator( + self, + text: $text, + isFirstResponder: $isFirstResponder, + lineSpacingMultiplier: lineSpacing + ) } func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { - let attributedString = NSMutableAttributedString(string: text) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = lineSpacing - attributedString.addAttribute( - NSAttributedString.Key.paragraphStyle, - value: paragraphStyle, - range: NSMakeRange(0, attributedString.length) // swiftlint:disable:this legacy_constructor - ) + uiView.text = text - uiView.attributedText = attributedString let font = textStyle let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle) uiView.font = fontMetrics.scaledFont(for: font) diff --git a/iOS/PostEditor/PostTextEditingView.swift b/iOS/PostEditor/PostTextEditingView.swift index 0f85e84..74b6857 100644 --- a/iOS/PostEditor/PostTextEditingView.swift +++ b/iOS/PostEditor/PostTextEditingView.swift @@ -70,9 +70,9 @@ struct PostTextEditingView: View { text: $post.body, textStyle: $bodyTextStyle, isFirstResponder: $bodyIsFirstResponder, - lineSpacing: 17 * ( - horizontalSizeClass == .compact ? bodyLineSpacingMultiplier / 2 : bodyLineSpacingMultiplier - ) + lineSpacing: horizontalSizeClass == .compact + ? bodyLineSpacingMultiplier / 2 + : bodyLineSpacingMultiplier ) .onChange(of: post.body) { _ in if post.status == PostStatus.published.rawValue && !updatingBodyFromServer { diff --git a/iOS/PostEditor/PostTitleTextView.swift b/iOS/PostEditor/PostTitleTextView.swift index 434722c..72af11c 100644 --- a/iOS/PostEditor/PostTitleTextView.swift +++ b/iOS/PostEditor/PostTitleTextView.swift @@ -3,7 +3,7 @@ import SwiftUI -class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate { +class PostTitleCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate { @Binding var text: String @Binding var isFirstResponder: Bool var didBecomeFirstResponder: Bool = false @@ -17,7 +17,7 @@ class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate { _isFirstResponder = isFirstResponder } - func textViewDidChangeSelection(_ textView: UITextView) { + func textViewDidChange(_ textView: UITextView) { DispatchQueue.main.async { self.postTitleTextView.text = textView.text ?? "" } @@ -75,7 +75,7 @@ struct PostTitleTextView: UIViewRepresentable { return textView } - func makeCoordinator() -> Coordinator { + func makeCoordinator() -> PostTitleCoordinator { return Coordinator(self, text: $text, isFirstResponder: $isFirstResponder) } From 3134965b2c3ae3b0431c5fc54b43471ad0c7e5b3 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Wed, 28 Oct 2020 13:23:41 -0400 Subject: [PATCH 06/10] Add (dynamic) line spacing back to post body editor --- iOS/PostEditor/PostBodyTextView.swift | 17 +++++++++++++++-- iOS/PostEditor/PostEditorView.swift | 1 - iOS/PostEditor/PostTextEditingView.swift | 6 ++---- iOS/PostEditor/PostTitleTextView.swift | 7 ++----- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/iOS/PostEditor/PostBodyTextView.swift b/iOS/PostEditor/PostBodyTextView.swift index 2eaef56..7530fd5 100644 --- a/iOS/PostEditor/PostBodyTextView.swift +++ b/iOS/PostEditor/PostBodyTextView.swift @@ -34,7 +34,20 @@ class PostBodyCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect ) -> CGFloat { - return 17 * lineSpacingMultiplier + // 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 + } } } @@ -42,7 +55,7 @@ struct PostBodyTextView: UIViewRepresentable { @Binding var text: String @Binding var textStyle: UIFont @Binding var isFirstResponder: Bool - var lineSpacing: CGFloat + @State var lineSpacing: CGFloat func makeUIView(context: UIViewRepresentableContext) -> UITextView { let textView = UITextView(frame: .zero) diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift index 818d4bd..36d05f6 100644 --- a/iOS/PostEditor/PostEditorView.swift +++ b/iOS/PostEditor/PostEditorView.swift @@ -3,7 +3,6 @@ import SwiftUI struct PostEditorView: View { @EnvironmentObject var model: WriteFreelyModel @Environment(\.managedObjectContext) var moc - @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.presentationMode) var presentationMode @ObservedObject var post: WFAPost diff --git a/iOS/PostEditor/PostTextEditingView.swift b/iOS/PostEditor/PostTextEditingView.swift index 74b6857..eede6da 100644 --- a/iOS/PostEditor/PostTextEditingView.swift +++ b/iOS/PostEditor/PostTextEditingView.swift @@ -11,7 +11,7 @@ struct PostTextEditingView: View { @State private var titleIsFirstResponder: Bool = true @State private var bodyTextStyle: UIFont = UIFont(name: "Lora-Regular", size: 17)! @State private var bodyIsFirstResponder: Bool = false - private let bodyLineSpacingMultiplier: CGFloat = 0.5 + private let lineSpacingMultiplier: CGFloat = 0.5 init( post: ObservedObject, @@ -70,9 +70,7 @@ struct PostTextEditingView: View { text: $post.body, textStyle: $bodyTextStyle, isFirstResponder: $bodyIsFirstResponder, - lineSpacing: horizontalSizeClass == .compact - ? bodyLineSpacingMultiplier / 2 - : bodyLineSpacingMultiplier + lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier ) .onChange(of: post.body) { _ in if post.status == PostStatus.published.rawValue && !updatingBodyFromServer { diff --git a/iOS/PostEditor/PostTitleTextView.swift b/iOS/PostEditor/PostTitleTextView.swift index 72af11c..531feee 100644 --- a/iOS/PostEditor/PostTitleTextView.swift +++ b/iOS/PostEditor/PostTitleTextView.swift @@ -1,5 +1,4 @@ -// Based on https://lostmoa.com/blog/DynamicHeightForTextFieldInSwiftUI/ -// and https://stackoverflow.com/a/56508132/1234545 +// Based on https://lostmoa.com/blog/DynamicHeightForTextFieldInSwiftUI and https://stackoverflow.com/a/56508132/1234545 import SwiftUI @@ -37,9 +36,7 @@ class PostTitleCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegat atEnd layoutFinishedFlag: Bool ) { DispatchQueue.main.async { - guard let view = self.textView else { - return - } + 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 From c2df4cab8672ab81926e9bd28b1a7e44515ef347 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Thu, 29 Oct 2020 10:36:43 -0400 Subject: [PATCH 07/10] Default to all-posts list if logged in, drafts otherwise --- Shared/Navigation/ContentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index ee33945..426b112 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -7,7 +7,7 @@ struct ContentView: View { NavigationView { SidebarView() - PostListView(selectedCollection: nil, showAllPosts: true) + PostListView(selectedCollection: nil, showAllPosts: model.account.isLoggedIn) Text("Select a post, or create a new local draft.") .foregroundColor(.secondary) From dbf2ecda60dd143717ab4353bd5843a468e9e403 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Thu, 29 Oct 2020 10:37:16 -0400 Subject: [PATCH 08/10] Show only drafts list if logged out --- Shared/PostCollection/CollectionListView.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index e8e50e9..9dbcf68 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -11,10 +11,10 @@ struct CollectionListView: View { var body: some View { List { - NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: true)) { - Text("All Posts") - } if model.account.isLoggedIn { + NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: true)) { + Text("All Posts") + } NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) { Text(model.account.server == "https://write.as" ? "Anonymous" : "Drafts") } @@ -27,6 +27,10 @@ struct CollectionListView: View { } } } + } else { + NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) { + Text("Drafts") + } } } .navigationTitle( From c44d48e5263f209561f2d72fc5005aeb44b0ea91 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Mon, 2 Nov 2020 14:15:50 -0500 Subject: [PATCH 09/10] Add line spacing to title text field --- iOS/PostEditor/PostTextEditingView.swift | 3 +- iOS/PostEditor/PostTitleTextView.swift | 38 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/iOS/PostEditor/PostTextEditingView.swift b/iOS/PostEditor/PostTextEditingView.swift index eede6da..a58fbae 100644 --- a/iOS/PostEditor/PostTextEditingView.swift +++ b/iOS/PostEditor/PostTextEditingView.swift @@ -46,7 +46,8 @@ struct PostTextEditingView: View { text: $post.title, textStyle: $titleTextStyle, height: $titleTextHeight, - isFirstResponder: $titleIsFirstResponder + isFirstResponder: $titleIsFirstResponder, + lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier ) .frame(height: titleFieldHeight) .onChange(of: post.title) { _ in diff --git a/iOS/PostEditor/PostTitleTextView.swift b/iOS/PostEditor/PostTitleTextView.swift index 531feee..510ea31 100644 --- a/iOS/PostEditor/PostTitleTextView.swift +++ b/iOS/PostEditor/PostTitleTextView.swift @@ -5,15 +5,22 @@ 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, isFirstResponder: Binding) { + init( + _ textView: PostTitleTextView, + text: Binding, + isFirstResponder: Binding, + lineSpacingMultiplier: CGFloat + ) { self.postTitleTextView = textView _text = text _isFirstResponder = isFirstResponder + self.lineSpacingMultiplier = lineSpacingMultiplier } func textViewDidChange(_ textView: UITextView) { @@ -43,6 +50,27 @@ class PostTitleCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegat } } } + + 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 { @@ -50,6 +78,7 @@ struct PostTitleTextView: UIViewRepresentable { @Binding var textStyle: UIFont @Binding var height: CGFloat @Binding var isFirstResponder: Bool + @State var lineSpacing: CGFloat func makeUIView(context: UIViewRepresentableContext) -> UITextView { let textView = UITextView() @@ -73,7 +102,12 @@ struct PostTitleTextView: UIViewRepresentable { } func makeCoordinator() -> PostTitleCoordinator { - return Coordinator(self, text: $text, isFirstResponder: $isFirstResponder) + return Coordinator( + self, + text: $text, + isFirstResponder: $isFirstResponder, + lineSpacingMultiplier: lineSpacing + ) } func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { From 51f8495a06edd2100848c5808788b47e215d72f2 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Mon, 2 Nov 2020 15:03:31 -0500 Subject: [PATCH 10/10] Make Return key navigate from title field to last body field position --- iOS/PostEditor/PostBodyTextView.swift | 22 ++++++++++++++++++++++ iOS/PostEditor/PostTextEditingView.swift | 2 ++ iOS/PostEditor/PostTitleTextView.swift | 1 + 3 files changed, 25 insertions(+) diff --git a/iOS/PostEditor/PostBodyTextView.swift b/iOS/PostEditor/PostBodyTextView.swift index 7530fd5..4dfa0b2 100644 --- a/iOS/PostEditor/PostBodyTextView.swift +++ b/iOS/PostEditor/PostBodyTextView.swift @@ -5,6 +5,7 @@ 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 @@ -15,12 +16,14 @@ class PostBodyCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate _ textView: PostBodyTextView, text: Binding, isFirstResponder: Binding, + currentTextPosition: Binding, lineSpacingMultiplier: CGFloat ) { self.postBodyTextView = textView _text = text _isFirstResponder = isFirstResponder self.lineSpacingMultiplier = lineSpacingMultiplier + _currentTextPosition = currentTextPosition } func textViewDidChange(_ textView: UITextView) { @@ -29,6 +32,23 @@ class PostBodyCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate } } + 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, @@ -55,6 +75,7 @@ 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) -> UITextView { @@ -82,6 +103,7 @@ struct PostBodyTextView: UIViewRepresentable { self, text: $text, isFirstResponder: $isFirstResponder, + currentTextPosition: $currentTextPosition, lineSpacingMultiplier: lineSpacing ) } diff --git a/iOS/PostEditor/PostTextEditingView.swift b/iOS/PostEditor/PostTextEditingView.swift index a58fbae..90a1e5c 100644 --- a/iOS/PostEditor/PostTextEditingView.swift +++ b/iOS/PostEditor/PostTextEditingView.swift @@ -11,6 +11,7 @@ struct PostTextEditingView: View { @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( @@ -71,6 +72,7 @@ struct PostTextEditingView: View { text: $post.body, textStyle: $bodyTextStyle, isFirstResponder: $bodyIsFirstResponder, + currentTextPosition: $bodyCursorPosition, lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier ) .onChange(of: post.body) { _ in diff --git a/iOS/PostEditor/PostTitleTextView.swift b/iOS/PostEditor/PostTitleTextView.swift index 510ea31..53d8bb8 100644 --- a/iOS/PostEditor/PostTitleTextView.swift +++ b/iOS/PostEditor/PostTitleTextView.swift @@ -32,6 +32,7 @@ class PostTitleCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegat func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { if text == "\n" { self.isFirstResponder.toggle() + self.didBecomeFirstResponder = false return false } return true