From 22d4809a24c5b5bc49efada7a43bf0e36e10379b Mon Sep 17 00:00:00 2001 From: Chris Offner <chrisoffner@pm.me> Date: Mon, 29 Mar 2021 17:24:39 +0200 Subject: [PATCH] Implemented Pull to Refresh. --- enzevalos_iphone.xcodeproj/project.pbxproj | 12 +-- enzevalos_iphone/SearchHelper.swift | 56 +++++------- .../SwiftUI/Inbox/InboxRefreshModel.swift | 66 ++++++++++++++ .../SwiftUI/Inbox/InboxView.swift | 54 ++--------- .../SwiftUI/Inbox/MailListView.swift | 90 +++++++++---------- .../SwiftUI/Inbox/RefreshModel.swift | 29 ------ .../SwiftUI/Inbox/UpdateMailsModel.swift | 7 +- 7 files changed, 145 insertions(+), 169 deletions(-) create mode 100644 enzevalos_iphone/SwiftUI/Inbox/InboxRefreshModel.swift delete mode 100644 enzevalos_iphone/SwiftUI/Inbox/RefreshModel.swift diff --git a/enzevalos_iphone.xcodeproj/project.pbxproj b/enzevalos_iphone.xcodeproj/project.pbxproj index cd445386..568990ed 100644 --- a/enzevalos_iphone.xcodeproj/project.pbxproj +++ b/enzevalos_iphone.xcodeproj/project.pbxproj @@ -27,8 +27,7 @@ 3EB4FAA420120096001D0625 /* DialogOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EB4FAA320120096001D0625 /* DialogOption.swift */; }; 3EC35F2420037651008BDF95 /* InvitationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC35F2320037651008BDF95 /* InvitationHelper.swift */; }; 3EC35F302003838E008BDF95 /* InvitationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC35F2F2003838E008BDF95 /* InvitationTests.swift */; }; - 3F071ECF2611B03A00E121F0 /* RefreshModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F071ECE2611B03A00E121F0 /* RefreshModel.swift */; }; - 3F071ED32611BF6900E121F0 /* UpdateMailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F071ED22611BF6900E121F0 /* UpdateMailsModel.swift */; }; + 3F071ED32611BF6900E121F0 /* InboxRefreshModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F071ED22611BF6900E121F0 /* InboxRefreshModel.swift */; }; 3FB75DC525FFA75C00919925 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB75DC425FFA75B00919925 /* ComposeView.swift */; }; 3FB75DC925FFA77800919925 /* RecipientRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB75DC825FFA77800919925 /* RecipientRowView.swift */; }; 3FB75DCD25FFD37400919925 /* RecipientListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB75DCC25FFD37400919925 /* RecipientListView.swift */; }; @@ -304,8 +303,7 @@ 3EB4FAA320120096001D0625 /* DialogOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DialogOption.swift; sourceTree = "<group>"; }; 3EC35F2320037651008BDF95 /* InvitationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InvitationHelper.swift; path = Invitation/InvitationHelper.swift; sourceTree = "<group>"; }; 3EC35F2F2003838E008BDF95 /* InvitationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitationTests.swift; sourceTree = "<group>"; }; - 3F071ECE2611B03A00E121F0 /* RefreshModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshModel.swift; sourceTree = "<group>"; }; - 3F071ED22611BF6900E121F0 /* UpdateMailsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMailsModel.swift; sourceTree = "<group>"; }; + 3F071ED22611BF6900E121F0 /* InboxRefreshModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxRefreshModel.swift; sourceTree = "<group>"; }; 3FB75DC425FFA75B00919925 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; }; 3FB75DC825FFA77800919925 /* RecipientRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientRowView.swift; sourceTree = "<group>"; }; 3FB75DCC25FFD37400919925 /* RecipientListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipientListView.swift; sourceTree = "<group>"; }; @@ -971,8 +969,7 @@ 4750BDE02539C5FC00F6D5AB /* InboxView.swift */, 47FA8EAB254D77DE006883D0 /* MailListView.swift */, 47C3490125489F52008D290C /* MailRowView.swift */, - 3F071ECE2611B03A00E121F0 /* RefreshModel.swift */, - 3F071ED22611BF6900E121F0 /* UpdateMailsModel.swift */, + 3F071ED22611BF6900E121F0 /* InboxRefreshModel.swift */, ); path = Inbox; sourceTree = "<group>"; @@ -1894,7 +1891,7 @@ 4775D7AC243F18BC0052F2CC /* SecurityBriefingView.swift in Sources */, 971D404A2428C87E002FCD31 /* BadgeCaseView.swift in Sources */, 47C8225B24379EAE005BCE73 /* MessageViewMain.swift in Sources */, - 3F071ED32611BF6900E121F0 /* UpdateMailsModel.swift in Sources */, + 3F071ED32611BF6900E121F0 /* InboxRefreshModel.swift in Sources */, 47FAE30E2524AA97005A1BCB /* DataModel.xcdatamodeld in Sources */, 47A5D6E22294BF3B0084F81D /* TempKey.swift in Sources */, 47C8226B2438A86B005BCE73 /* SenderViewMain.swift in Sources */, @@ -1970,7 +1967,6 @@ 47BCAF5A259DE6770008FE4B /* SecretKeyListView.swift in Sources */, A15D215B223BE5F4003E0CE0 /* TempAttachment.swift in Sources */, 4706D65F225B7B6B00B3F1D3 /* ItunesHandler.swift in Sources */, - 3F071ECF2611B03A00E121F0 /* RefreshModel.swift in Sources */, 47EABF0A241A9C8700774A93 /* AuthenticationViewModel.swift in Sources */, 47FA8EAC254D77DE006883D0 /* MailListView.swift in Sources */, 47C8225924379EAE005BCE73 /* AttPreview.swift in Sources */, diff --git a/enzevalos_iphone/SearchHelper.swift b/enzevalos_iphone/SearchHelper.swift index b6bffdc3..bce506aa 100644 --- a/enzevalos_iphone/SearchHelper.swift +++ b/enzevalos_iphone/SearchHelper.swift @@ -8,51 +8,39 @@ import Foundation -/** - A collection of helper methods that are used for the different search bars - */ - -/** - Function to be used to find mails that contain the search terms. All terms (separated by spaces) need to be contained in the search text. - - parameters: - - content: The String that will be searched - - searchText: Search terms (space-separated) that will be searched for -*/ -func containsSearchTerms ( content : String?, searchText: String) -> Bool -{ - guard searchText.count > 0 else { - ///Case empty search - return true +/// Function used to find mails that contain the search terms. All terms (separated by spaces) need to be contained in the search text. +/// - Parameters: +/// - content: The String that will be searched +/// - searchText: Search terms (space-separated) that will be searched for +func containsSearchTerms(content: String?, searchTerm: String) -> Bool { + guard searchTerm.count > 0 else { + return true // Case empty search } + guard let content = content else { - //Case Mail has no body/subject - return false + return false // Case Mail has no body/subject } - var longterms : [String] = [] - var terms : [String] = [] - //Break String into substrings separated by quoatation marks - longterms = searchText.components(separatedBy: "\"") + var longterms: [String] = [] + var terms: [String] = [] + + // Break string into substrings separated by quotation marks + longterms = searchTerm.components(separatedBy: "\"") + + // Even elements will be outside the quotation marks and need to be separated again var i = 0 - //even elements will be outside the quotation marks and need to be separated again - while (i < longterms.count) - { - if i % 2 == 0 - { + while (i < longterms.count) { + if i % 2 == 0 { terms.append(contentsOf: longterms[i].lowercased().components(separatedBy: " ")) - } - else - { + } else { terms.append(longterms[i].lowercased()) } - i+=1 + i += 1 } var found = true - for t in terms - { - if !(t == "") - { + for t in terms { + if !t.isEmpty { found = found && content.lowercased().contains(t) } } diff --git a/enzevalos_iphone/SwiftUI/Inbox/InboxRefreshModel.swift b/enzevalos_iphone/SwiftUI/Inbox/InboxRefreshModel.swift new file mode 100644 index 00000000..0fe04858 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/InboxRefreshModel.swift @@ -0,0 +1,66 @@ +// +// InboxRefreshModel.swift +// enzevalos_iphone +// +// Created by Chris Offner on 29.03.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import Foundation + +/// Model for Pull to Refresh and updating inbox. +class InboxRefreshModel: ObservableObject { + @Published var updating = false // updating inbox + @Published var refreshStarted = false // pull to refresh sequence started + @Published var pullReleased = false + var folderPath: String + var pullOffsetBaseline: CGFloat = 0 + var pullOffset: CGFloat = 0 + + init(folderPath: String) { + self.folderPath = folderPath + } + + /// Text representation of inbox update state. + var lastUpdate: String { + var text = NSLocalizedString("Updating", comment: "updating...") + + if !updating { + let last = Date() + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.timeStyle = .medium + let dateString = dateFormatter.string(from: last) + text = NSLocalizedString("LastUpdate", comment: "") + " " + dateString + } + + return text + } + + /// Retrieves new emails and updates the respective email folder. + func updateMails() { + guard !updating else { + return + } + + LetterboxModel + .instance + .mailHandler + .updateFolder(folderpath: folderPath) { error in + if error == nil { + self.updating = false + } + // TODO: Add error message + } + updating = true + } + + /// Refreshes inbox and resets pull to refresh states. + func refresh() { + DispatchQueue.main.async { + self.updateMails() + self.pullReleased = false + self.refreshStarted = false + } + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/InboxView.swift b/enzevalos_iphone/SwiftUI/Inbox/InboxView.swift index 2ba3a58f..056ad5ac 100644 --- a/enzevalos_iphone/SwiftUI/Inbox/InboxView.swift +++ b/enzevalos_iphone/SwiftUI/Inbox/InboxView.swift @@ -8,9 +8,7 @@ // import SwiftUI -import CoreData -// TODO: Refactor to Model! -// Updating text -> Last update, updating, no connection... +//import CoreData struct InboxView: View { var folderPath: String @@ -18,23 +16,23 @@ struct InboxView: View { @State private var updating = false @State private var composeMail = false @State private var goToFolders = false - @ObservedObject var mailUpdater: UpdateMailsModel + @ObservedObject var refreshModel: InboxRefreshModel init(folderPath: String, name: String) { self.folderPath = folderPath self.name = name - self.mailUpdater = UpdateMailsModel(folderPath: folderPath) + self.refreshModel = InboxRefreshModel(folderPath: folderPath) } var body: some View { mailListView - .onAppear(perform: mailUpdater.updateMails) + .onAppear(perform: refreshModel.updateMails) .sheet(isPresented: $composeMail) { ComposeView() } .navigationBarItems(trailing: keyManagementButton) .toolbar { ToolbarItem(placement: .status) { - Button(action: mailUpdater.updateMails) { - Text(mailUpdater.lastUpdate).font(.callout) + Button(action: refreshModel.updateMails) { + Text(refreshModel.lastUpdate) } } @@ -46,7 +44,7 @@ struct InboxView: View { } private var mailListView: some View { - MailListView(folderPath: folderPath, folderName: name, mailUpdater: mailUpdater) + MailListView(folderPath: folderPath, folderName: name, refreshModel: refreshModel) .environment(\.managedObjectContext, PersistentDataProvider .dataProvider @@ -70,44 +68,6 @@ struct InboxView: View { } } - // // TODO: Refactor lastUpdate to model - // private var lastUpdate: some View { - // var text = NSLocalizedString("Updating", comment: "updating...") - // - // if !updating { - // let last = Date() - // let dateFormatter = DateFormatter() - // dateFormatter.locale = Locale.current - // dateFormatter.timeStyle = .medium - // let dateString = dateFormatter.string(from: last) - // text = NSLocalizedString("LastUpdate", comment: "") + " " + dateString - // } - // - // return Button(action: updateMails) { - // Text(text).font(.callout) - // } - // } - // - // - // // TODO: Refactor updateMails to model - // private func updateMails() { - // guard !updating else { - // return - // } - // - // LetterboxModel - // .instance - // .mailHandler - // .updateFolder(folderpath: folderPath) { error in - // if error == nil { - // self.updating = false - // } - // // TODO: Add error message - // } - // updating = true - // } - - private var folderButton: some View { Button { goToFolders = true diff --git a/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift b/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift index 8a4291a3..f116eb33 100644 --- a/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift +++ b/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift @@ -9,42 +9,38 @@ import SwiftUI import CoreData -// TODO: Refactor to Model! -// Updating text -> Last update, updating, no connection.... +/// A view that lists emails in a particular folder together with a search function. struct MailListView: View { @Environment(\.managedObjectContext) var managedObjectContext - @ObservedObject var model = RefreshModel() var fetchRequest: FetchRequest<MailRecord> var mails: FetchedResults<MailRecord>{fetchRequest.wrappedValue} var folderPath: String var folderName: String - var mailUpdater: UpdateMailsModel - - @State private var showUser = false + @ObservedObject var model: InboxRefreshModel @State private var searchText = "" @State private var searchType = SearchType.All - init(folderPath: String, folderName: String, mailUpdater: UpdateMailsModel) { + init(folderPath: String, folderName: String, refreshModel: InboxRefreshModel) { fetchRequest = MailRecord.mailsInFolderFetchRequest(folderpath: folderPath) self.folderPath = folderPath self.folderName = folderName - self.mailUpdater = mailUpdater + self.model = refreshModel } var body: some View { - VStack(alignment: .leading) { - SearchView(searchText: $searchText, searchType: $searchType) - .padding() + VStack(alignment: .leading, spacing: 0) { + SearchView(searchText: $searchText, searchType: $searchType).padding() ScrollView { - measurements.frame(width: 0, height: 0) + refreshTrigger.frame(width: 0, height: 0) - VStack { + ZStack(alignment: topCenter) { + mailList.frame(height: 600) pullToRefreshIndicator - mailList } - .offset(y: model.released ? 50 : -10) + .offset(y: model.updating ? 35 : -10) + .animation(.default) } } .navigationBarTitle(folderName, displayMode: .inline) @@ -66,44 +62,46 @@ struct MailListView: View { @ViewBuilder private var pullToRefreshIndicator: some View { - if model.started && model.released { - ProgressView() - .offset(y: -32) + if model.updating { + ProgressView().offset(y: -32) } else { Image(systemName: "arrow.down.circle") .foregroundColor(.secondary) - .font(.system(size: 28, weight: .bold)) - .rotationEffect(Angle(degrees: model.started ? 180 : 0)) + .font(.system(size: 28, weight: .regular)) + .rotationEffect(Angle(degrees: model.refreshStarted ? 180 : 0)) .offset(y: -32) .animation(.easeIn) } } - private var measurements: GeometryReader<AnyView> { + private var topCenter: Alignment { + Alignment(horizontal: .center, vertical: .top) + } + + /// Measures pulldown offset and triggers inbox refresh. + private var refreshTrigger: GeometryReader<AnyView> { GeometryReader { geo -> AnyView in DispatchQueue.main.async { - if model.startOffset == 0 { - model.startOffset = geo.frame(in: .global).minY + // Set initial baseline for pulldown based on dynamic layout + if model.pullOffsetBaseline == 0 { + model.pullOffsetBaseline = geo.frame(in: .global).minY } - model.offset = geo.frame(in: .global).minY + // Update offset dynamically as user pulls down + model.pullOffset = geo.frame(in: .global).minY - if model.offset - model.startOffset > 80 && !model.started { - model.started = true + // Indicate refresh start if pulldown threshold is reached + if model.pullOffset - model.pullOffsetBaseline > 80 && !model.refreshStarted { + model.refreshStarted = true } - if model.offset == model.startOffset && model.started { - - if !model.released { - withAnimation(.linear) { - model.released = true - model.refresh(perform: mailUpdater.updateMails) - } - } else { - if model.disabled { - model.disabled = false - model.refresh(perform: mailUpdater.updateMails) - } + // Trigger refresh once pulldown is fully released + if model.pullOffset == model.pullOffsetBaseline + && model.refreshStarted + && !model.pullReleased { + withAnimation(.linear) { + model.pullReleased = true + model.refresh() } } } @@ -112,6 +110,7 @@ struct MailListView: View { } } + /// Filters emails based on search criteria. func filterKeyRecord(keyRecord: MailRecord) -> Bool { if self.searchText.isEmpty || self.searchText == NSLocalizedString("Searchbar.Title", comment: "Search") { @@ -120,19 +119,20 @@ struct MailListView: View { let query = self.searchText.lowercased() if (searchType == .All || searchType == .Sender) - && (containsSearchTerms(content: keyRecord.sender.displayname, searchText: query) - || containsSearchTerms(content: keyRecord.sender.email, searchText: query)) { + && (containsSearchTerms(content: keyRecord.sender.displayname, searchTerm: query) + || containsSearchTerms(content: keyRecord.sender.email, searchTerm: query)) { return true } else if (searchType == .All || searchType == .Sender) - && keyRecord.addresses.filter({ - containsSearchTerms(content: $0.email, - searchText: query) }).count > 0 { + && (keyRecord.addresses.filter { + containsSearchTerms(content: $0.email, + searchTerm: query) } + .count > 0) { return true } else if (searchType == .All || searchType == .Subject) - && containsSearchTerms(content: keyRecord.subject, searchText: query) { + && containsSearchTerms(content: keyRecord.subject, searchTerm: query) { return true } else if (searchType == .All || searchType == .Body) - && containsSearchTerms(content: keyRecord.body, searchText: query) { + && containsSearchTerms(content: keyRecord.body, searchTerm: query) { return true } return false diff --git a/enzevalos_iphone/SwiftUI/Inbox/RefreshModel.swift b/enzevalos_iphone/SwiftUI/Inbox/RefreshModel.swift deleted file mode 100644 index e58f5fdf..00000000 --- a/enzevalos_iphone/SwiftUI/Inbox/RefreshModel.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// RefreshModel.swift -// enzevalos_iphone -// -// Created by Chris Offner on 29.03.21. -// Copyright © 2021 fu-berlin. All rights reserved. -// - -import Foundation - -class RefreshModel: ObservableObject { - var startOffset: CGFloat = 0 - var offset: CGFloat = 0 - var started = false - var released = false - var disabled = false - - func refresh(perform action: @escaping () -> Void) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if self.offset == self.startOffset { - action() - self.released = false - self.started = false - } else { - self.disabled = true - } - } - } -} diff --git a/enzevalos_iphone/SwiftUI/Inbox/UpdateMailsModel.swift b/enzevalos_iphone/SwiftUI/Inbox/UpdateMailsModel.swift index b94a4cbc..7359c830 100644 --- a/enzevalos_iphone/SwiftUI/Inbox/UpdateMailsModel.swift +++ b/enzevalos_iphone/SwiftUI/Inbox/UpdateMailsModel.swift @@ -8,7 +8,7 @@ import Foundation -class UpdateMailsModel: ObservableObject { +class InboxRefreshModel: ObservableObject { @Published var updating = false var folderPath: String @@ -48,8 +48,3 @@ class UpdateMailsModel: ObservableObject { updating = true } } - - - - - -- GitLab