From 573acb561cde6ad9cde8aae7e223a099e06ba093 Mon Sep 17 00:00:00 2001 From: Callum Yarnold Date: Tue, 24 May 2022 22:09:26 +0100 Subject: [PATCH] Add support for iOS 14.0 --- ntfy.xcodeproj/project.pbxproj | 22 +- ntfy/Extensions/View.swift | 26 +++ ntfy/Views/NotificationListView.swift | 288 ++++++++++++++------------ ntfy/Views/SubscriptionAddView.swift | 3 +- ntfy/Views/SubscriptionListView.swift | 163 +++++++++------ 5 files changed, 295 insertions(+), 207 deletions(-) create mode 100644 ntfy/Extensions/View.swift diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index 59622ab..df9a2d3 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 02024E60283D7CBB0064224A /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02024E5F283D7CBB0064224A /* View.swift */; }; 9474F1C1282F2AA700CDE4DD /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1C0282F2AA700CDE4DD /* AppMain.swift */; }; 9474F1C3282F2AA700CDE4DD /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F1C2282F2AA700CDE4DD /* ContentView.swift */; }; 9474F1C5282F2AA800CDE4DD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9474F1C4282F2AA800CDE4DD /* Assets.xcassets */; }; @@ -62,6 +63,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 02024E5F283D7CBB0064224A /* View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; 9474F1BD282F2AA700CDE4DD /* ntfy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ntfy.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9474F1C0282F2AA700CDE4DD /* AppMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMain.swift; sourceTree = ""; }; 9474F1C2282F2AA700CDE4DD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -108,6 +110,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 02024E5E283D7CBB0064224A /* Extensions */ = { + isa = PBXGroup; + children = ( + 02024E5F283D7CBB0064224A /* View.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 9474F1B4282F2AA700CDE4DD = { isa = PBXGroup; children = ( @@ -130,6 +140,7 @@ 9474F1BF282F2AA700CDE4DD /* ntfy */ = { isa = PBXGroup; children = ( + 02024E5E283D7CBB0064224A /* Extensions */, 9474F210283326E000CDE4DD /* Utils */, 9474F20D2833264F00CDE4DD /* App */, 9474F1C4282F2AA800CDE4DD /* Assets.xcassets */, @@ -318,6 +329,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 02024E60283D7CBB0064224A /* View.swift in Sources */, 9474F1F92830835400CDE4DD /* Store.swift in Sources */, 9474F212283327C200CDE4DD /* Helpers.swift in Sources */, 9474F217283531A300CDE4DD /* Log.swift in Sources */, @@ -413,7 +425,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -468,7 +480,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -497,6 +509,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -529,6 +542,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -553,7 +567,7 @@ INFOPLIST_FILE = ntfyNSE/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfyNSE; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 15.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -580,7 +594,7 @@ INFOPLIST_FILE = ntfyNSE/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ntfyNSE; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 15.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ntfy/Extensions/View.swift b/ntfy/Extensions/View.swift new file mode 100644 index 0000000..9987895 --- /dev/null +++ b/ntfy/Extensions/View.swift @@ -0,0 +1,26 @@ +// +// View.swift +// ntfy +// +// Created by Callum Yarnold on 24/05/2022. +// + +import SwiftUI + +struct disableAutoCapitalisationModifier: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 15.0, *) { + content + .textInputAutocapitalization(.never) + } else { + content + .autocapitalization(.none) + } + } +} + +extension View { + func disableAutoCapitalisation() -> some View { + modifier(disableAutoCapitalisationModifier()) + } +} diff --git a/ntfy/Views/NotificationListView.swift b/ntfy/Views/NotificationListView.swift index d4f77ef..f6b3352 100644 --- a/ntfy/Views/NotificationListView.swift +++ b/ntfy/Views/NotificationListView.swift @@ -23,121 +23,133 @@ struct NotificationListView: View { } var body: some View { - List(selection: $selection) { - ForEach(subscription.notificationsSorted(), id: \.self) { notification in - NotificationRowView(notification: notification) - } - } - .listStyle(PlainListStyle()) - .navigationBarTitleDisplayMode(.inline) - .environment(\.editMode, self.$editMode) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - if (self.editMode != .active) { - Button(action: { - // iOS bug (?): We create a custom back button, because when we return using the original - // back button, and the navigation is popped that way, the row stays highlighted for a long - // time, which is weird and feels wrong. This avoids that behavior. - - self.delegate.selectedBaseUrl = nil - }){ - Image(systemName: "chevron.left") - } - .padding([.top, .bottom, .trailing], 20) - } - } - ToolbarItem(placement: .principal) { - Text(subscription.displayName()).font(.headline) - } - ToolbarItem(placement: .navigationBarTrailing) { - if (self.editMode == .active) { - editButton - } else { - Menu { - if subscription.notificationCount() > 0 { - editButton - } - Button("Send test notification") { - self.sendTestNotification() - } - if subscription.notificationCount() > 0 { - Button("Clear all notifications") { - self.showAlert = true - self.activeAlert = .clear - } - } - Button("Unsubscribe") { - self.showAlert = true - self.activeAlert = .unsubscribe - } - } label: { - Image(systemName: "ellipsis.circle") - .padding([.top, .bottom, .leading], 20) - } - } - } - ToolbarItem(placement: .navigationBarLeading) { - if (self.editMode == .active) { - Button(action: { - self.showAlert = true - self.activeAlert = .selected - }) { - Text("Delete") - .foregroundColor(.red) - } - } - } - } - .alert(isPresented: $showAlert) { - switch activeAlert { - case .clear: - return Alert( - title: Text("Clear notifications"), - message: Text("Do you really want to delete all of the notifications in this topic?"), - primaryButton: .destructive( - Text("Permanently delete"), - action: deleteAll - ), - secondaryButton: .cancel()) - case .unsubscribe: - return Alert( - title: Text("Unsubscribe"), - message: Text("Do you really want to unsubscribe from this topic and delete all of the notifications you received?"), - primaryButton: .destructive( - Text("Unsubscribe"), - action: unsubscribe - ), - secondaryButton: .cancel()) - case .selected: - return Alert( - title: Text("Delete"), - message: Text("Do you really want to delete these selected notifications?"), - primaryButton: .destructive( - Text("Delete"), - action: deleteSelected - ), - secondaryButton: .cancel()) - } - } - .overlay(Group { - if subscription.notificationCount() == 0 { - VStack { - Text("You haven't received any notifications for this topic yet.") - .font(.title2) - .foregroundColor(.gray) - .multilineTextAlignment(.center) - .padding(.bottom) - Text("To send notifications to this topic, simply PUT or POST to the topic URL.\n\nExample:\n`$ curl -d \"hi\" ntfy.sh/\(subscription.topicName())`\n\nDetailed instructions are available on [ntfy.sh](https;//ntfy.sh) and [in the docs](https:ntfy.sh/docs).") - .foregroundColor(.gray) - } - .padding(40) - } - }) - .refreshable { - subscriptionManager.poll(subscription) - } + notificationlist + if #available(iOS 15.0, *) { + notificationlist + .refreshable { + subscriptionManager.poll(subscription) + } + } else { + notificationlist + } } + + private var notificationlist: some View { + List(selection: $selection) { + ForEach(subscription.notificationsSorted(), id: \.self) { notification in + NotificationRowView(notification: notification) + } + } + .listStyle(PlainListStyle()) + .navigationBarTitleDisplayMode(.inline) + .environment(\.editMode, self.$editMode) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + if (self.editMode != .active) { + Button(action: { + // iOS bug (?): We create a custom back button, because when we return using the original + // back button, and the navigation is popped that way, the row stays highlighted for a long + // time, which is weird and feels wrong. This avoids that behavior. + + self.delegate.selectedBaseUrl = nil + }){ + Image(systemName: "chevron.left") + } + .padding([.top, .bottom, .trailing], 20) + } + } + ToolbarItem(placement: .principal) { + Text(subscription.displayName()).font(.headline) + } + ToolbarItem(placement: .navigationBarTrailing) { + if (self.editMode == .active) { + editButton + } else { + Menu { + Button("Refresh") { + subscriptionManager.poll(subscription) + } + if subscription.notificationCount() > 0 { + editButton + } + Button("Send test notification") { + self.sendTestNotification() + } + if subscription.notificationCount() > 0 { + Button("Clear all notifications") { + self.showAlert = true + self.activeAlert = .clear + } + } + Button("Unsubscribe") { + self.showAlert = true + self.activeAlert = .unsubscribe + } + } label: { + Image(systemName: "ellipsis.circle") + .padding([.top, .bottom, .leading], 20) + } + } + } + ToolbarItem(placement: .navigationBarLeading) { + if (self.editMode == .active) { + Button(action: { + self.showAlert = true + self.activeAlert = .selected + }) { + Text("Delete") + .foregroundColor(.red) + } + } + } + } + .alert(isPresented: $showAlert) { + switch activeAlert { + case .clear: + return Alert( + title: Text("Clear notifications"), + message: Text("Do you really want to delete all of the notifications in this topic?"), + primaryButton: .destructive( + Text("Permanently delete"), + action: deleteAll + ), + secondaryButton: .cancel()) + case .unsubscribe: + return Alert( + title: Text("Unsubscribe"), + message: Text("Do you really want to unsubscribe from this topic and delete all of the notifications you received?"), + primaryButton: .destructive( + Text("Unsubscribe"), + action: unsubscribe + ), + secondaryButton: .cancel()) + case .selected: + return Alert( + title: Text("Delete"), + message: Text("Do you really want to delete these selected notifications?"), + primaryButton: .destructive( + Text("Delete"), + action: deleteSelected + ), + secondaryButton: .cancel()) + } + } + .overlay(Group { + if subscription.notificationCount() == 0 { + VStack { + Text("You haven't received any notifications for this topic yet.") + .font(.title2) + .foregroundColor(.gray) + .multilineTextAlignment(.center) + .padding(.bottom) + Text("To send notifications to this topic, simply PUT or POST to the topic URL.\n\nExample:\n`$ curl -d \"hi\" ntfy.sh/\(subscription.topicName())`\n\nDetailed instructions are available on [ntfy.sh](https;//ntfy.sh) and [in the docs](https:ntfy.sh/docs).") + .foregroundColor(.gray) + } + .padding(40) + } + }) + } private var editButton: some View { if editMode == .inactive { @@ -199,27 +211,35 @@ struct NotificationRowView: View { @ObservedObject var notification: Notification var body: some View { - VStack(alignment: .leading, spacing: 0) { - Text(notification.shortDateTime()) - .font(.subheadline) - .foregroundColor(.gray) - if let title = notification.title, title != "" { - Text(title) - .font(.headline) - .bold() - } - Text(notification.message ?? "") - .font(.body) - } - .padding(.all, 4) - .swipeActions(edge: .trailing) { - Button(role: .destructive) { - store.delete(notification: notification) - } label: { - Label("Delete", systemImage: "trash.circle") - } - } + if #available(iOS 15.0, *) { + notificationRow + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + store.delete(notification: notification) + } label: { + Label("Delete", systemImage: "trash.circle") + } + } + } else { + notificationRow + } } + + private var notificationRow: some View { + VStack(alignment: .leading, spacing: 0) { + Text(notification.shortDateTime()) + .font(.subheadline) + .foregroundColor(.gray) + if let title = notification.title, title != "" { + Text(title) + .font(.headline) + .bold() + } + Text(notification.message ?? "") + .font(.body) + } + .padding(.all, 4) + } } struct NotificationListView_Previews: PreviewProvider { diff --git a/ntfy/Views/SubscriptionAddView.swift b/ntfy/Views/SubscriptionAddView.swift index b3e6ba4..6c594ff 100644 --- a/ntfy/Views/SubscriptionAddView.swift +++ b/ntfy/Views/SubscriptionAddView.swift @@ -20,7 +20,8 @@ struct SubscriptionAddView: View { footer: Text("Topics may not be password protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications") ) { TextField("Topic name, e.g. phil_alerts", text: $topic) - .textInputAutocapitalization(.never) + .disableAutoCapitalisation() + .disableAutocorrection(true) } } .navigationBarTitleDisplayMode(.inline) diff --git a/ntfy/Views/SubscriptionListView.swift b/ntfy/Views/SubscriptionListView.swift index 2e5457b..243af80 100644 --- a/ntfy/Views/SubscriptionListView.swift +++ b/ntfy/Views/SubscriptionListView.swift @@ -15,42 +15,61 @@ struct SubscriptionListView: View { var body: some View { NavigationView { - List { - ForEach(subscriptions) { subscription in - SubscriptionItemNavView(subscription: subscription) - } - } - .listStyle(PlainListStyle()) - .navigationTitle("Subscribed topics") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - NavigationLink(destination: SubscriptionAddView()) { - Image(systemName: "plus") - } - } - } - .overlay(Group { - if subscriptions.isEmpty { - VStack { - Text("It looks like you don't have any subscriptions yet") - .font(.title2) - .foregroundColor(.gray) - .multilineTextAlignment(.center) - .padding(.bottom) - Text("Click the + to create or subscribe to a topic. Afterwards, you receive notifications on your device when sending messages via PUT or POST.\n\nDetailed instructions are available on [ntfy.sh](https;//ntfy.sh) and [in the docs](https:ntfy.sh/docs).") - .foregroundColor(.gray) - } - .padding(40) - } - }) - .refreshable { - subscriptions.forEach { subscription in - subscriptionManager.poll(subscription) - } - } + if #available(iOS 15.0, *) { + subscriptionList + .refreshable { + subscriptions.forEach { subscription in + subscriptionManager.poll(subscription) + } + } + } else { + subscriptionList + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + subscriptions.forEach { subscription in + subscriptionManager.poll(subscription) + } + } label: { + Image(systemName: "arrow.clockwise") + } + } + } + } } .navigationViewStyle(StackNavigationViewStyle()) } + + private var subscriptionList: some View { + List { + ForEach(subscriptions) { subscription in + SubscriptionItemNavView(subscription: subscription) + } + } + .listStyle(PlainListStyle()) + .navigationTitle("Subscribed topics") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + NavigationLink(destination: SubscriptionAddView()) { + Image(systemName: "plus") + } + } + } + .overlay(Group { + if subscriptions.isEmpty { + VStack { + Text("It looks like you don't have any subscriptions yet") + .font(.title2) + .foregroundColor(.gray) + .multilineTextAlignment(.center) + .padding(.bottom) + Text("Click the + to create or subscribe to a topic. Afterwards, you receive notifications on your device when sending messages via PUT or POST.\n\nDetailed instructions are available on [ntfy.sh](https;//ntfy.sh) and [in the docs](https:ntfy.sh/docs).") + .foregroundColor(.gray) + } + .padding(40) + } + }) + } } struct SubscriptionItemNavView: View { @@ -64,41 +83,49 @@ struct SubscriptionItemNavView: View { } var body: some View { - ZStack { - NavigationLink( - destination: NotificationListView(subscription: subscription), - tag: subscription.urlString(), - selection: $delegate.selectedBaseUrl - ) { - EmptyView() - } - .opacity(0.0) - .buttonStyle(PlainButtonStyle()) - - SubscriptionItemRowView(subscription: subscription) - } - .swipeActions(edge: .trailing) { - Button(role: .destructive) { - self.unsubscribeAlert = true - } label: { - Label("Delete", systemImage: "trash.circle") - } - } - .alert(isPresented: $unsubscribeAlert) { - Alert( - title: Text("Unsubscribe"), - message: Text("Do you really want to unsubscribe from this topic and delete all of the notifications you received?"), - primaryButton: .destructive( - Text("Unsubscribe"), - action: { - self.subscriptionManager.unsubscribe(subscription) - self.unsubscribeAlert = false - } - ), - secondaryButton: .cancel() - ) - } - } + if #available(iOS 15.0, *) { + subscriptionRow + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + self.unsubscribeAlert = true + } label: { + Label("Delete", systemImage: "trash.circle") + } + } + } else { + subscriptionRow + } + } + + private var subscriptionRow: some View { + ZStack { + NavigationLink( + destination: NotificationListView(subscription: subscription), + tag: subscription.urlString(), + selection: $delegate.selectedBaseUrl + ) { + EmptyView() + } + .opacity(0.0) + .buttonStyle(PlainButtonStyle()) + + SubscriptionItemRowView(subscription: subscription) + } + .alert(isPresented: $unsubscribeAlert) { + Alert( + title: Text("Unsubscribe"), + message: Text("Do you really want to unsubscribe from this topic and delete all of the notifications you received?"), + primaryButton: .destructive( + Text("Unsubscribe"), + action: { + self.subscriptionManager.unsubscribe(subscription) + self.unsubscribeAlert = false + } + ), + secondaryButton: .cancel() + ) + } + } } struct SubscriptionItemRowView: View {