Raise deployment target to iOS 18 and modernize SwiftUI APIs

Adopt modern SwiftUI patterns now that the minimum target is iOS 18:
NavigationStack, .toolbar, .tint, new Tab API with sidebarAdaptable
style, and remove iOS 17 availability checks. Add Liquid Glass effect
support for iOS 26 in TimerView and fix an optional interpolation
warning in AppState.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 23:14:57 +01:00
parent 512d534edf
commit 527acd2967
11 changed files with 209 additions and 70 deletions

View File

@@ -10,46 +10,40 @@ import SwiftUI
struct MainView: View {
@StateObject var appState = AppState()
@StateObject var groceryList = GroceryList()
// Tab ViewModels
@StateObject var recipeViewModel = RecipeTabView.ViewModel()
@StateObject var searchViewModel = SearchTabView.ViewModel()
@State private var selectedTab: Tab = .recipes
enum Tab {
case recipes, search, groceryList
}
var body: some View {
TabView {
RecipeTabView()
.environmentObject(recipeViewModel)
.environmentObject(appState)
.environmentObject(groceryList)
.tabItem {
Label("Recipes", systemImage: "book.closed.fill")
}
.tag(Tab.recipes)
SearchTabView()
.environmentObject(searchViewModel)
.environmentObject(appState)
.environmentObject(groceryList)
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
.tag(Tab.search)
GroceryListTabView()
.environmentObject(groceryList)
.tabItem {
if #available(iOS 17.0, *) {
Label("Grocery List", systemImage: "storefront")
} else {
Label("Grocery List", systemImage: "heart.text.square")
}
}
.tag(Tab.groceryList)
TabView(selection: $selectedTab) {
SwiftUI.Tab("Recipes", systemImage: "book.closed.fill", value: .recipes) {
RecipeTabView()
.environmentObject(recipeViewModel)
.environmentObject(appState)
.environmentObject(groceryList)
}
SwiftUI.Tab("Search", systemImage: "magnifyingglass", value: .search, role: .search) {
SearchTabView()
.environmentObject(searchViewModel)
.environmentObject(appState)
.environmentObject(groceryList)
}
SwiftUI.Tab("Grocery List", systemImage: "storefront", value: .groceryList) {
GroceryListTabView()
.environmentObject(groceryList)
}
}
.tabViewStyle(.sidebarAdaptable)
.modifier(TabBarMinimizeModifier())
.task {
recipeViewModel.presentLoadingIndicator = true
await appState.getCategories()

View File

@@ -148,7 +148,7 @@ struct BorderedLoginTextField: View {
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.foregroundColor(color)
.accentColor(color)
.tint(color)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
@@ -170,7 +170,7 @@ struct LoginTextField: View {
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.foregroundColor(color)
.accentColor(color)
.tint(color)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)

View File

@@ -187,12 +187,17 @@ struct WebViewSheet: View {
@State var url: String
var body: some View {
NavigationView {
NavigationStack {
WebView(url: URL(string: url)!)
.navigationBarTitle(Text("Nextcloud Login"), displayMode: .inline)
.navigationBarItems(trailing: Button("Done") {
dismiss()
})
.navigationTitle("Nextcloud Login")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
}
}

View File

@@ -69,7 +69,7 @@ struct EditableListView: View {
@State var axis: Axis = .vertical
var body: some View {
NavigationView {
NavigationStack {
ZStack {
List {
if items.isEmpty {
@@ -83,12 +83,12 @@ struct EditableListView: View {
.onDelete(perform: deleteItem)
.onMove(perform: moveItem)
.scrollDismissesKeyboard(.immediately)
}
}
VStack {
Spacer()
Button {
addItem()
} label: {
@@ -101,12 +101,15 @@ struct EditableListView: View {
.padding()
}
}
.navigationBarTitle(title, displayMode: .inline)
.navigationBarItems(
trailing: Button(action: { isPresented = false }) {
Text("Done")
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(action: { isPresented = false }) {
Text("Done")
}
}
)
}
.environment(\.editMode, .constant(.active))
}
}

View File

@@ -30,11 +30,7 @@ struct RecipeIngredientSection: View {
}
}
} label: {
if #available(iOS 17.0, *) {
Image(systemName: "storefront")
} else {
Image(systemName: "heart.text.square")
}
Image(systemName: "storefront")
}.disabled(viewModel.editMode)
SecondaryLabel(text: LocalizedStringKey("Ingredients"))
@@ -111,14 +107,8 @@ fileprivate struct IngredientListItem: View {
var body: some View {
HStack(alignment: .top) {
if groceryList.containsItem(at: recipeId, item: ingredient) {
if #available(iOS 17.0, *) {
Image(systemName: "storefront")
.foregroundStyle(Color.green)
} else {
Image(systemName: "heart.text.square")
.foregroundStyle(Color.green)
}
Image(systemName: "storefront")
.foregroundStyle(Color.green)
} else if isSelected {
Image(systemName: "checkmark.circle")
} else {

View File

@@ -55,8 +55,13 @@ struct TimerView: View {
.bold()
.padding()
.background {
RoundedRectangle(cornerRadius: 20)
.foregroundStyle(.ultraThickMaterial)
if #available(iOS 26, *) {
Color.clear
.glassEffect(.regular, in: .rect(cornerRadius: 20))
} else {
RoundedRectangle(cornerRadius: 20)
.foregroundStyle(.ultraThickMaterial)
}
}
}
}

View File

@@ -0,0 +1,28 @@
//
// LiquidGlassModifiers.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 14.02.26.
//
import SwiftUI
struct TabBarMinimizeModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 26, *) {
content.tabBarMinimizeBehavior(.onScrollDown)
} else {
content
}
}
}
struct BackgroundExtensionModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 26, *) {
content.backgroundExtensionEffect()
} else {
content
}
}
}

View File

@@ -72,7 +72,7 @@ struct RecipeTabView: View {
)
.id(category.id) // Workaround: This is needed to update the detail view when the selection changes
}
}
}
.tint(.nextcloudBlue)