From 896b5f73f78f03e4fd772899882872e7abb3ac09 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sat, 25 Jul 2020 07:02:11 -0400 Subject: [PATCH] Implement basic post store and list / editor UI components --- .swiftlint.yml | 4 +- Shared/ContentView.swift | 13 ---- Shared/Extensions/View+Keyboard.swift | 9 +++ Shared/Navigation/ContentView.swift | 27 +++++++ Shared/Post/Post.swift | 53 +++++++++++++ Shared/Post/PostCell.swift | 41 ++++++++++ Shared/Post/PostEditor.swift | 41 ++++++++++ Shared/Post/PostList.swift | 21 +++++ Shared/Post/PostStatusBadge.swift | 63 +++++++++++++++ Shared/Post/PostStore.swift | 11 +++ Shared/WriteFreely_MultiPlatformApp.swift | 3 +- .../project.pbxproj | 90 +++++++++++++++++++++- 12 files changed, 360 insertions(+), 16 deletions(-) delete mode 100644 Shared/ContentView.swift create mode 100644 Shared/Extensions/View+Keyboard.swift create mode 100644 Shared/Navigation/ContentView.swift create mode 100644 Shared/Post/Post.swift create mode 100644 Shared/Post/PostCell.swift create mode 100644 Shared/Post/PostEditor.swift create mode 100644 Shared/Post/PostList.swift create mode 100644 Shared/Post/PostStatusBadge.swift create mode 100644 Shared/Post/PostStore.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 6931ea5..921d334 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,2 +1,4 @@ type_name: - allowed_symbols: ["_"] # Used in SwiftUI boilerplate naming + allowed_symbols: ["_"] # Used in SwiftUI boilerplate naming +identifier_name: + excluded: ["id"] # Required for Identifiable conformance diff --git a/Shared/ContentView.swift b/Shared/ContentView.swift deleted file mode 100644 index 1297731..0000000 --- a/Shared/ContentView.swift +++ /dev/null @@ -1,13 +0,0 @@ -import SwiftUI - -struct ContentView: View { - var body: some View { - Text("Hello, WriteFreely!").padding() - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/Shared/Extensions/View+Keyboard.swift b/Shared/Extensions/View+Keyboard.swift new file mode 100644 index 0000000..a24c81c --- /dev/null +++ b/Shared/Extensions/View+Keyboard.swift @@ -0,0 +1,9 @@ +import SwiftUI + +#if canImport(UIKit) +extension View { + func hideKeyboard() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } +} +#endif diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift new file mode 100644 index 0000000..3bb8a26 --- /dev/null +++ b/Shared/Navigation/ContentView.swift @@ -0,0 +1,27 @@ +import SwiftUI + +struct ContentView: View { + @ObservedObject var postStore: PostStore + + var body: some View { + NavigationView { + PostList(postStore: postStore) + .frame(maxHeight: .infinity) + .navigationTitle("Posts") + .toolbar { + NavigationLink(destination: PostEditor()) { + Image(systemName: "plus") + } + } + + Text("Select a post, or create a new draft.") + .foregroundColor(.secondary) + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView(postStore: testPostStore) + } +} diff --git a/Shared/Post/Post.swift b/Shared/Post/Post.swift new file mode 100644 index 0000000..e1bf6e3 --- /dev/null +++ b/Shared/Post/Post.swift @@ -0,0 +1,53 @@ +import Foundation +import WriteFreely + +struct Post: Identifiable { + var id = UUID() + var title: String + var body: String + var createdDate: Date + var status: PostStatus = .draft + var editableText: String { + return """ + # \(self.title) + + \(self.body) + """ + } +} + +let testPost = Post( + title: "Test Post Title", + body: """ + Here's some cool sample body text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ultrices \ + posuere dignissim. Vestibulum a libero tempor, lacinia nulla vitae, congue purus. Nunc ac nulla quam. Duis \ + tincidunt eros augue, et volutpat tortor pulvinar ut. Nullam sit amet maximus urna. Phasellus non dignissim lacus.\ + Nulla ac posuere ex. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec \ + non molestie mauris. Suspendisse potenti. Vivamus at erat turpis. + + Pellentesque porttitor gravida tincidunt. Sed vitae eros non metus aliquam hendrerit. Aliquam sed risus suscipit \ + turpis dictum dictum. Duis lacus lectus, dictum vel felis in, rhoncus fringilla felis. Nunc id dolor nisl. Aliquam \ + euismod purus elit. Nullam egestas neque leo, sed aliquet ligula ultrices nec. + """, + createdDate: Date(), + status: .published) + +let testPostData = [ + Post( + title: "My First Post", + body: "Look at me, creating a first post! That's cool.", + createdDate: Date(timeIntervalSince1970: 1595429452), + status: .published + ), + Post( + title: "Post 2: The Quickening", + body: "See, here's the rule about Highlander jokes: _there can be only one_.", + createdDate: Date(timeIntervalSince1970: 1595514125), + status: .edited + ), + Post( + title: "The Post Revolutions", + body: "I can never keep the Matrix movie order straight. Why not just call them part 2 and part 3?", + createdDate: Date(timeIntervalSince1970: 1595600006) + ) +] diff --git a/Shared/Post/PostCell.swift b/Shared/Post/PostCell.swift new file mode 100644 index 0000000..1a2a0cd --- /dev/null +++ b/Shared/Post/PostCell.swift @@ -0,0 +1,41 @@ +import SwiftUI + +struct PostCell: View { + var post: Post + var body: some View { + NavigationLink( + destination: PostEditor( + textString: post.editableText, + postStatus: post.status + ) + ) { + HStack { + VStack(alignment: .leading) { + Text(post.title) + .font(.headline) + .lineLimit(1) + Text(buildDateString(from: post.createdDate)) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) + } + Spacer() + PostStatusBadge(postStatus: post.status) + } + } + } + + func buildDateString(from date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .short + + return dateFormatter.string(from: date) + } +} + +struct PostCell_Previews: PreviewProvider { + static var previews: some View { + PostCell(post: testPost) + } +} diff --git a/Shared/Post/PostEditor.swift b/Shared/Post/PostEditor.swift new file mode 100644 index 0000000..b47dc99 --- /dev/null +++ b/Shared/Post/PostEditor.swift @@ -0,0 +1,41 @@ +// +// PostEditor.swift +// WriteFreely-MultiPlatform +// +// Created by Angelo Stavrow on 2020-07-24. +// + +import SwiftUI + +struct PostEditor: View { + @State var textString: String = "" + @State private var hasUnpublishedChanges: Bool = false + var postStatus: PostStatus = .draft + + var body: some View { + TextEditor(text: $textString.animation()) + .font(.body) + .padding() + .onChange(of: textString) { _ in + if postStatus == .published { + hasUnpublishedChanges = true + } + } + .toolbar { + if hasUnpublishedChanges { + PostStatusBadge(postStatus: .edited) + } else { + PostStatusBadge(postStatus: postStatus) + } + } + } +} + +struct PostEditor_Previews: PreviewProvider { + static var previews: some View { + PostEditor( + textString: testPost.editableText, + postStatus: testPost.status + ) + } +} diff --git a/Shared/Post/PostList.swift b/Shared/Post/PostList.swift new file mode 100644 index 0000000..8c2e81e --- /dev/null +++ b/Shared/Post/PostList.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct PostList: View { + var postStore: PostStore + + var body: some View { + List { + Text("\(postStore.posts.count) Posts") + .foregroundColor(.secondary) + ForEach(postStore.posts) { post in + PostCell(post: post) + } + } + } +} + +struct PostList_Previews: PreviewProvider { + static var previews: some View { + PostList(postStore: testPostStore) + } +} diff --git a/Shared/Post/PostStatusBadge.swift b/Shared/Post/PostStatusBadge.swift new file mode 100644 index 0000000..03275c4 --- /dev/null +++ b/Shared/Post/PostStatusBadge.swift @@ -0,0 +1,63 @@ +import SwiftUI + +enum PostStatus { + case draft + case edited + case published +} + +struct PostStatusBadge: View { + @State var postStatus: PostStatus + + var body: some View { + let (badgeLabel, badgeColor) = setupBadgeProperties(for: postStatus) + Text(badgeLabel) + .font(.caption) + .fontWeight(.bold) + .foregroundColor(.white) + .textCase(.uppercase) + .lineLimit(1) + .padding(EdgeInsets(top: 2.5, leading: 7.5, bottom: 2.5, trailing: 7.5)) + .background(badgeColor) + .clipShape(RoundedRectangle(cornerRadius: 5.0, style: .circular)) + } + + func setupBadgeProperties(for status: PostStatus) -> (String, Color) { + var badgeLabel: String + var badgeColor: Color + + switch status { + case .draft: + badgeLabel = "draft" + badgeColor = Color(red: 0.75, green: 0.5, blue: 0.85, opacity: 1.0) + case .edited: + badgeLabel = "edited" + badgeColor = Color(red: 0.75, green: 0.7, blue: 0.1, opacity: 1.0) + case .published: + badgeLabel = "published" + badgeColor = .gray + } + + return (badgeLabel, badgeColor) + } +} + +struct PostStatusBadge_DraftPreviews: PreviewProvider { + static var previews: some View { + PostStatusBadge(postStatus: .draft) + } +} + +struct PostStatusBadge_EditedPreviews: PreviewProvider { + static var previews: some View { + Group { + PostStatusBadge(postStatus: .edited) + } + } +} + +struct PostStatusBadge_PublishedPreviews: PreviewProvider { + static var previews: some View { + PostStatusBadge(postStatus: .published) + } +} diff --git a/Shared/Post/PostStore.swift b/Shared/Post/PostStore.swift new file mode 100644 index 0000000..03916ba --- /dev/null +++ b/Shared/Post/PostStore.swift @@ -0,0 +1,11 @@ +import Foundation + +class PostStore: ObservableObject { + @Published var posts: [Post] + + init(posts: [Post] = []) { + self.posts = posts + } +} + +let testPostStore = PostStore(posts: testPostData) diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift index bc1f3ed..1004d26 100644 --- a/Shared/WriteFreely_MultiPlatformApp.swift +++ b/Shared/WriteFreely_MultiPlatformApp.swift @@ -2,9 +2,10 @@ import SwiftUI @main struct WriteFreely_MultiPlatformApp: App { + @StateObject private var store = PostStore() var body: some Scene { WindowGroup { - ContentView() + ContentView(postStore: store) } } } diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index 04faa5c..e4697b6 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -7,6 +7,17 @@ objects = { /* Begin PBXBuildFile section */ + 1756AE6B24CB1E4B00FD7257 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE6A24CB1E4B00FD7257 /* Post.swift */; }; + 1756AE6C24CB1E4B00FD7257 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE6A24CB1E4B00FD7257 /* Post.swift */; }; + 1756AE6E24CB255B00FD7257 /* PostStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE6D24CB255B00FD7257 /* PostStore.swift */; }; + 1756AE6F24CB255B00FD7257 /* PostStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE6D24CB255B00FD7257 /* PostStore.swift */; }; + 1756AE7424CB26FA00FD7257 /* PostCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7324CB26FA00FD7257 /* PostCell.swift */; }; + 1756AE7524CB26FA00FD7257 /* PostCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7324CB26FA00FD7257 /* PostCell.swift */; }; + 1756AE7724CB2EDD00FD7257 /* PostEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7624CB2EDD00FD7257 /* PostEditor.swift */; }; + 1756AE7824CB2EDD00FD7257 /* PostEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7624CB2EDD00FD7257 /* PostEditor.swift */; }; + 1756AE7A24CB65DF00FD7257 /* PostList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7924CB65DF00FD7257 /* PostList.swift */; }; + 1756AE7B24CB65DF00FD7257 /* PostList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7924CB65DF00FD7257 /* PostList.swift */; }; + 1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE8024CB844500FD7257 /* View+Keyboard.swift */; }; 17DF329D24C87D3500BCE2E3 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF329C24C87D3500BCE2E3 /* Tests_iOS.swift */; }; 17DF32A824C87D3500BCE2E3 /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF32A724C87D3500BCE2E3 /* Tests_macOS.swift */; }; 17DF32AA24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */; }; @@ -17,6 +28,8 @@ 17DF32AF24C87D3500BCE2E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17DF328324C87D3500BCE2E3 /* Assets.xcassets */; }; 17DF32C024C87D7B00BCE2E3 /* WriteFreely in Frameworks */ = {isa = PBXBuildFile; productRef = 17DF32BF24C87D7B00BCE2E3 /* WriteFreely */; }; 17DF32C324C87D8D00BCE2E3 /* WriteFreely in Frameworks */ = {isa = PBXBuildFile; productRef = 17DF32C224C87D8D00BCE2E3 /* WriteFreely */; }; + 17DF32D524C8CA3400BCE2E3 /* PostStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF32D424C8CA3400BCE2E3 /* PostStatusBadge.swift */; }; + 17DF32D624C8CA3400BCE2E3 /* PostStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF32D424C8CA3400BCE2E3 /* PostStatusBadge.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -37,6 +50,12 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1756AE6A24CB1E4B00FD7257 /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; + 1756AE6D24CB255B00FD7257 /* PostStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostStore.swift; sourceTree = ""; }; + 1756AE7324CB26FA00FD7257 /* PostCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCell.swift; sourceTree = ""; }; + 1756AE7624CB2EDD00FD7257 /* PostEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditor.swift; sourceTree = ""; }; + 1756AE7924CB65DF00FD7257 /* PostList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostList.swift; sourceTree = ""; }; + 1756AE8024CB844500FD7257 /* View+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Keyboard.swift"; sourceTree = ""; }; 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteFreely_MultiPlatformApp.swift; sourceTree = ""; }; 17DF328224C87D3300BCE2E3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 17DF328324C87D3500BCE2E3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -56,6 +75,7 @@ 17DF32C824C8854B00BCE2E3 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; 17DF32C924C8855E00BCE2E3 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 17DF32CA24C8856C00BCE2E3 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; + 17DF32D424C8CA3400BCE2E3 /* PostStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostStatusBadge.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -92,6 +112,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1756AE7F24CB841200FD7257 /* Extensions */ = { + isa = PBXGroup; + children = ( + 1756AE8024CB844500FD7257 /* View+Keyboard.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 17DF327B24C87D3300BCE2E3 = { isa = PBXGroup; children = ( @@ -114,8 +142,13 @@ isa = PBXGroup; children = ( 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */, - 17DF328224C87D3300BCE2E3 /* ContentView.swift */, 17DF328324C87D3500BCE2E3 /* Assets.xcassets */, + 17DF32D024C8B75C00BCE2E3 /* Model */, + 1756AE7F24CB841200FD7257 /* Extensions */, + 17DF32CC24C8B72300BCE2E3 /* Navigation */, + 17DF32D224C8B78D00BCE2E3 /* Collection */, + 17DF32D124C8B78500BCE2E3 /* Post */, + 17DF32D324C8C9F600BCE2E3 /* Components */, ); path = Shared; sourceTree = ""; @@ -173,6 +206,48 @@ name = Frameworks; sourceTree = ""; }; + 17DF32CC24C8B72300BCE2E3 /* Navigation */ = { + isa = PBXGroup; + children = ( + 17DF328224C87D3300BCE2E3 /* ContentView.swift */, + ); + path = Navigation; + sourceTree = ""; + }; + 17DF32D024C8B75C00BCE2E3 /* Model */ = { + isa = PBXGroup; + children = ( + ); + path = Model; + sourceTree = ""; + }; + 17DF32D124C8B78500BCE2E3 /* Post */ = { + isa = PBXGroup; + children = ( + 1756AE6A24CB1E4B00FD7257 /* Post.swift */, + 1756AE7324CB26FA00FD7257 /* PostCell.swift */, + 1756AE7624CB2EDD00FD7257 /* PostEditor.swift */, + 1756AE7924CB65DF00FD7257 /* PostList.swift */, + 1756AE6D24CB255B00FD7257 /* PostStore.swift */, + 17DF32D424C8CA3400BCE2E3 /* PostStatusBadge.swift */, + ); + path = Post; + sourceTree = ""; + }; + 17DF32D224C8B78D00BCE2E3 /* Collection */ = { + isa = PBXGroup; + children = ( + ); + path = Collection; + sourceTree = ""; + }; + 17DF32D324C8C9F600BCE2E3 /* Components */ = { + isa = PBXGroup; + children = ( + ); + path = Components; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -379,7 +454,14 @@ buildActionMask = 2147483647; files = ( 17DF32AC24C87D3500BCE2E3 /* ContentView.swift in Sources */, + 1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */, + 1756AE7724CB2EDD00FD7257 /* PostEditor.swift in Sources */, + 17DF32D524C8CA3400BCE2E3 /* PostStatusBadge.swift in Sources */, + 1756AE7A24CB65DF00FD7257 /* PostList.swift in Sources */, 17DF32AA24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */, + 1756AE6E24CB255B00FD7257 /* PostStore.swift in Sources */, + 1756AE6B24CB1E4B00FD7257 /* Post.swift in Sources */, + 1756AE7424CB26FA00FD7257 /* PostCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -388,7 +470,13 @@ buildActionMask = 2147483647; files = ( 17DF32AD24C87D3500BCE2E3 /* ContentView.swift in Sources */, + 1756AE7824CB2EDD00FD7257 /* PostEditor.swift in Sources */, + 17DF32D624C8CA3400BCE2E3 /* PostStatusBadge.swift in Sources */, + 1756AE7B24CB65DF00FD7257 /* PostList.swift in Sources */, 17DF32AB24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */, + 1756AE6F24CB255B00FD7257 /* PostStore.swift in Sources */, + 1756AE6C24CB1E4B00FD7257 /* Post.swift in Sources */, + 1756AE7524CB26FA00FD7257 /* PostCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };