From 298910f279dba7a43f1af69e67aee3e67e58970d Mon Sep 17 00:00:00 2001 From: ykisana Date: Fri, 13 Nov 2026 23:08:22 -0600 Subject: [PATCH] feat: support iOS and iPadOS 17.4 The minimum was iOS 18.0, which left out the three iPads that stop at iPadOS 17 (iPad 6th gen, iPad Pro 10.5", iPad Pro 12.9" 2nd gen) and anyone still on iOS 17. - Deployment target is 17.4 for every target. 17.4, not 17.0, because Max Daily Limits needs DeviceActivityEvent's includesPastActivity. - The main and Settings tab bars keep Tab on iOS 18 and later. On iOS 17, which doesn't have Tab, the else branch builds the same tabs with tabItem and tag. - Control Center controls need iOS 18, so they are only registered and reloaded there. On iOS 17 there are no Control Center controls, and the Unblock shortcuts with an NFC or QR key say "Open Normal to unblock." instead of opening Normal, as on iOS 18.0 and 18.1. --- Normal.xcodeproj/project.pbxproj | 20 +++---- Normal/Services/WidgetSync.swift | 4 +- Normal/Views/Settings/SettingsView.swift | 66 ++++++++++++++++++------ Normal/Views/TabView/MainTabView.swift | 22 +++++--- NormalWidget/BlockControls.swift | 3 ++ NormalWidget/NormalWidgetBundle.swift | 6 ++- 6 files changed, 87 insertions(+), 34 deletions(-) diff --git a/Normal.xcodeproj/project.pbxproj b/Normal.xcodeproj/project.pbxproj index 08d1336..c00ff82 100644 --- a/Normal.xcodeproj/project.pbxproj +++ b/Normal.xcodeproj/project.pbxproj @@ -652,7 +652,7 @@ INFOPLIST_FILE = NormalMonitor/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NormalMonitor; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -683,7 +683,7 @@ INFOPLIST_FILE = NormalMonitor/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NormalMonitor; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -755,7 +755,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -816,7 +816,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.7; MTL_ENABLE_DEBUG_INFO = NO; @@ -914,7 +914,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.NormalTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -935,7 +935,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.NormalTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -998,7 +998,7 @@ INFOPLIST_FILE = NormalWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Normal; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1030,7 +1030,7 @@ INFOPLIST_FILE = NormalWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Normal; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1061,7 +1061,7 @@ INFOPLIST_FILE = NormalShield/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NormalShield; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1092,7 +1092,7 @@ INFOPLIST_FILE = NormalShield/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NormalShield; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Normal/Services/WidgetSync.swift b/Normal/Services/WidgetSync.swift index ca5dde0..20a5349 100644 --- a/Normal/Services/WidgetSync.swift +++ b/Normal/Services/WidgetSync.swift @@ -30,7 +30,9 @@ enum WidgetSync { static func reloadTimelines() { WidgetCenter.shared.reloadAllTimelines() - ControlCenter.shared.reloadAllControls() + if #available(iOS 18.0, *) { + ControlCenter.shared.reloadAllControls() + } } } diff --git a/Normal/Views/Settings/SettingsView.swift b/Normal/Views/Settings/SettingsView.swift index b14bbd6..e06684c 100644 --- a/Normal/Views/Settings/SettingsView.swift +++ b/Normal/Views/Settings/SettingsView.swift @@ -30,6 +30,29 @@ struct SettingsView: View { var body: some View { NavigationStack { + tabView + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Close") { dismiss() } + } + } + .alert("Emergency Unblock", isPresented: $showConfirmation) { + Button("Unblock All Apps", role: .destructive, action: performEmergencyUnblock) + Button("Cancel", role: .cancel) {} + } message: { + Text("This will immediately remove all app blocks. Each unblock regenerates after 6 months. You have \(settings.emergencyUnblocksAvailable) remaining.") + } + .alert("All Apps Unblocked", isPresented: $showSuccessAlert) { + Button("OK", role: .cancel) {} + } message: { + Text("All app blocks have been removed. You have \(settings.emergencyUnblocksAvailable) emergency unblock\(settings.emergencyUnblocksAvailable == 1 ? "" : "s") remaining.") + } + } + } + + @ViewBuilder + private var tabView: some View { + if #available(iOS 18.0, *) { TabView(selection: $selectedTab) { Tab(SettingsTab.general.title, systemImage: SettingsTab.general.icon, value: SettingsTab.general) { GeneralSettingsView( @@ -58,21 +81,34 @@ struct SettingsView: View { .navigationTitle("Donate") } } - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Close") { dismiss() } - } - } - .alert("Emergency Unblock", isPresented: $showConfirmation) { - Button("Unblock All Apps", role: .destructive, action: performEmergencyUnblock) - Button("Cancel", role: .cancel) {} - } message: { - Text("This will immediately remove all app blocks. Each unblock regenerates after 6 months. You have \(settings.emergencyUnblocksAvailable) remaining.") - } - .alert("All Apps Unblocked", isPresented: $showSuccessAlert) { - Button("OK", role: .cancel) {} - } message: { - Text("All app blocks have been removed. You have \(settings.emergencyUnblocksAvailable) emergency unblock\(settings.emergencyUnblocksAvailable == 1 ? "" : "s") remaining.") + } else { + TabView(selection: $selectedTab) { + GeneralSettingsView( + settings: settings, + availableKeyTypes: availableKeyTypes + ) + .navigationTitle(SettingsTab.general.title) + .tabItem { Label(SettingsTab.general.title, systemImage: SettingsTab.general.icon) } + .tag(SettingsTab.general) + + EmergencyUnblockView( + settings: settings, + showConfirmation: $showConfirmation, + performEmergencyUnblock: performEmergencyUnblock + ) + .navigationTitle("Emergency Unblock") + .tabItem { Label(SettingsTab.emergencyUnblock.title, systemImage: SettingsTab.emergencyUnblock.icon) } + .tag(SettingsTab.emergencyUnblock) + + FAQView() + .navigationTitle("FAQ") + .tabItem { Label(SettingsTab.faq.title, systemImage: SettingsTab.faq.icon) } + .tag(SettingsTab.faq) + + DonationView() + .navigationTitle("Donate") + .tabItem { Label(SettingsTab.donation.title, systemImage: SettingsTab.donation.icon) } + .tag(SettingsTab.donation) } } } diff --git a/Normal/Views/TabView/MainTabView.swift b/Normal/Views/TabView/MainTabView.swift index ab6c5f8..f2de8a1 100644 --- a/Normal/Views/TabView/MainTabView.swift +++ b/Normal/Views/TabView/MainTabView.swift @@ -12,12 +12,22 @@ struct MainTabView: View { ] var body: some View { - TabView(selection: $selectedTab) { - Tab("Home", systemImage: "house", value: AppTab.home) { HomeView() } - Tab("Groups", systemImage: AppIcons.groups, value: AppTab.groups) { GroupsView() } - Tab("Schedules", systemImage: "calendar.badge.clock", value: AppTab.schedules) { SchedulesView() } - Tab("App Select", systemImage: "app.dashed", value: AppTab.appSelect) { AppSelectView() } - Tab("Keys", systemImage: "key.viewfinder", value: AppTab.keys) { KeysView() } + if #available(iOS 18.0, *) { + TabView(selection: $selectedTab) { + Tab("Home", systemImage: "house", value: AppTab.home) { HomeView() } + Tab("Groups", systemImage: AppIcons.groups, value: AppTab.groups) { GroupsView() } + Tab("Schedules", systemImage: "calendar.badge.clock", value: AppTab.schedules) { SchedulesView() } + Tab("App Select", systemImage: "app.dashed", value: AppTab.appSelect) { AppSelectView() } + Tab("Keys", systemImage: "key.viewfinder", value: AppTab.keys) { KeysView() } + } + } else { + TabView(selection: $selectedTab) { + HomeView().tabItem { Label("Home", systemImage: "house") }.tag(AppTab.home) + GroupsView().tabItem { Label("Groups", systemImage: AppIcons.groups) }.tag(AppTab.groups) + SchedulesView().tabItem { Label("Schedules", systemImage: "calendar.badge.clock") }.tag(AppTab.schedules) + AppSelectView().tabItem { Label("App Select", systemImage: "app.dashed") }.tag(AppTab.appSelect) + KeysView().tabItem { Label("Keys", systemImage: "key.viewfinder") }.tag(AppTab.keys) + } } } } diff --git a/NormalWidget/BlockControls.swift b/NormalWidget/BlockControls.swift index 8311f41..cf5692c 100644 --- a/NormalWidget/BlockControls.swift +++ b/NormalWidget/BlockControls.swift @@ -2,6 +2,7 @@ import AppIntents import SwiftUI import WidgetKit +@available(iOS 18.0, *) struct BlockAllControl: ControlWidget { static let kind = "BlockAllControl" @@ -17,6 +18,7 @@ struct BlockAllControl: ControlWidget { } } +@available(iOS 18.0, *) struct BlockGroupControl: ControlWidget { static let kind = "BlockGroupControl" @@ -39,6 +41,7 @@ struct BlockGroupControl: ControlWidget { } } +@available(iOS 18.0, *) struct BlockGroupControlConfiguration: ControlConfigurationIntent { static let title: LocalizedStringResource = "Block Group" diff --git a/NormalWidget/NormalWidgetBundle.swift b/NormalWidget/NormalWidgetBundle.swift index d78ef32..5fb7834 100644 --- a/NormalWidget/NormalWidgetBundle.swift +++ b/NormalWidget/NormalWidgetBundle.swift @@ -6,7 +6,9 @@ struct NormalWidgetBundle: WidgetBundle { var body: some Widget { GroupUnlockWidget() TimedUnblockLiveActivity() - BlockAllControl() - BlockGroupControl() + if #available(iOS 18.0, *) { + BlockAllControl() + BlockGroupControl() + } } }