Post

Replies

Boosts

Views

Activity

iOS 18 hit testing functionality differs from iOS 17
I created a Radar for this FB14766095, but thought I would add it here for extra visibility, or if anyone else had any thoughts on the issue. Basic Information Please provide a descriptive title for your feedback: iOS 18 hit testing functionality differs from iOS 17 What type of feedback are you reporting? Incorrect/Unexpected Behavior Description: Please describe the issue and what steps we can take to reproduce it: We have an issue in iOS 18 Beta 6 where hit testing functionality differs from the expected functionality in iOS 17.5.1 and previous versions of iOS. iOS 17: When a sheet is presented, the hit-testing logic considers subviews of the root view, meaning the rootView itself is rarely the hit view. iOS 18: When a sheet is presented, the hit-testing logic changes, sometimes considering the rootView itself as the hit view. Code: import SwiftUI struct ContentView: View { @State var isPresentingView: Bool = false var body: some View { VStack { Text("View One") Button { isPresentingView.toggle() } label: { Text("Present View Two") } } .padding() .sheet(isPresented: $isPresentingView) { ContentViewTwo() } } } #Preview { ContentView() } struct ContentViewTwo: View { @State var isPresentingView: Bool = false var body: some View { VStack { Text("View Two") } .padding() } } extension UIWindow { public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { /// Get view from superclass. guard let hitView = super.hitTest(point, with: event) else { return nil } print("RPTEST rootViewController = ", rootViewController.hashValue) print("RPTEST rootViewController?.view = ", rootViewController?.view.hashValue) print("RPTEST hitView = ", hitView.hashValue) if let rootView = rootViewController?.view { print("RPTEST rootViewController's view memory address: \(Unmanaged.passUnretained(rootView).toOpaque())") print("RPTEST hitView memory address: \(Unmanaged.passUnretained(hitView).toOpaque())") print("RPTEST Are they equal? \(rootView == hitView)") } /// If the returned view is the `UIHostingController`'s view, ignore. print("MTEST: hitTest rootViewController?.view == hitView", rootViewController?.view == hitView) print("MTEST: -") return hitView } } Looking at the print statements from the provided sample project:
 iOS 17 presenting a sheet from a button tap on the ContentView(): RPTEST rootViewController's view memory address: 0x0000000120009200 RPTEST hitView memory address: 0x000000011fd25000 RPTEST Are they equal? false MTEST: hitTest rootViewController?.view == hitView false RPTEST rootViewController's view memory address: 0x0000000120009200 RPTEST hitView memory address: 0x000000011fd25000 RPTEST Are they equal? false MTEST: hitTest rootViewController?.view == hitView false iOS 17 dismiss from presented view: RPTEST rootViewController's view memory address: 0x0000000120009200 RPTEST hitView memory address: 0x000000011fe04080 RPTEST Are they equal? false MTEST: hitTest rootViewController?.view == hitView false RPTEST rootViewController's view memory address: 0x0000000120009200 RPTEST hitView memory address: 0x000000011fe04080 RPTEST Are they equal? false MTEST: hitTest rootViewController?.view == hitView false iOS 18 presenting a sheet from a button tap on the ContentView(): RPTEST rootViewController's view memory address: 0x000000010333e3c0 RPTEST hitView memory address: 0x0000000103342080 RPTEST Are they equal? false MTEST: hitTest rootViewController?.view == hitView false RPTEST rootViewController's view memory address: 0x000000010333e3c0 RPTEST hitView memory address: 0x000000010333e3c0 RPTEST Are they equal? true MTEST: hitTest rootViewController?.view == hitView true You can see here ☝️ that in iOS 18 the views have the same memory address on the second call and are evaluated to be the same. This differs from iOS 17. iOS 18 dismiss RPTEST rootViewController's view memory address: 0x000000010333e3c0 RPTEST hitView memory address: 0x0000000103e80000 RPTEST Are they equal? false MTEST: hitTest rootViewController?.view == hitView false RPTEST rootViewController's view memory address: 0x000000010333e3c0 RPTEST hitView memory address: 0x0000000103e80000 RPTEST Are they equal? false MTEST: hitTest rootViewController?.view == hitView false The question I want to ask: Is this an intended change, meaning the current functionality in iOS 18 is expected? Or is this a bug and it's something that needs to be fixed? As a user, I would expect that the hit testing functionality would remain the same from iOS 17 to iOS 18. Thank you for your time.
12
11
1.8k
Aug ’24
iOS Keyboard Toolbar disappearing when consecutive sheets are presented
When adding a Done button to the keyboard toolbar the toolbar presents and operates as expected on the first view. If you present multiple views using .sheet() once on top of the other, with each keyboard using the same toolbar and Done button, when you get to the third view that has been presented, the toolbar disappears. I have tried adding a FocusState, FocusValue, NavigationStack and NavigationView, I have tried making sure the field state is cleared and contained within every view but nothing seems to work. Tested on Version 15.4 iPhone 15 pro sim import UIKit import SwiftUI struct ContentView: View { @Environment(\.dismiss) var dismiss @State var isPresenting: Bool = false @State var text: String = "" var body: some View { NavigationStack { VStack(alignment: .leading, spacing: 32) { TextField("Enter text", text: $text) .padding(.top, 20) .padding(.horizontal, 20) Button { isPresenting.toggle() } label: { Text("Present View") } .frame(maxWidth: .infinity, alignment: .center) .padding(.top, 40) Spacer() } .toolbar { toolbarContent } .sheet(isPresented: $isPresenting) { SecondView() } } } @ToolbarContentBuilder var toolbarContent: some ToolbarContent { ToolbarItemGroup(placement: .keyboard) { KeyboardAccessoryDone(title: "Done Content") } } } struct SecondView: View { @Environment(\.dismiss) var dismiss @State var isPresenting: Bool = false @State var text: String = "" var body: some View { NavigationStack { VStack(alignment: .leading, spacing: 32) { TextField("Enter text", text: $text) .padding(.top, 20) .padding(.horizontal, 20) Button { isPresenting.toggle() } label: { Text("Present View") } .frame(maxWidth: .infinity, alignment: .center) .padding(.top, 40) Spacer() } .toolbar { toolbarContent } .sheet(isPresented: $isPresenting) { ThirdView() } } } @ToolbarContentBuilder var toolbarContent: some ToolbarContent { ToolbarItem(placement: .topBarLeading) { CancelButton { dismiss() } } ToolbarItemGroup(placement: .keyboard) { KeyboardAccessoryDone(title: "Done Second") } } } struct ThirdView: View { @Environment(\.dismiss) var dismiss @State var isPresenting: Bool = false @State var text: String = "" var body: some View { NavigationStack { VStack(alignment: .leading, spacing: 32) { TextField("Enter text", text: $text) .padding(.top, 20) .padding(.horizontal, 20) Button { isPresenting.toggle() } label: { Text("Present View") } .frame(maxWidth: .infinity, alignment: .center) .padding(.top, 40) Spacer() } .toolbar { toolbarContent } .sheet(isPresented: $isPresenting) { FourthView() } } } @ToolbarContentBuilder var toolbarContent: some ToolbarContent { ToolbarItem(placement: .topBarLeading) { CancelButton { dismiss() } } ToolbarItemGroup(placement: .keyboard) { KeyboardAccessoryDone(title: "Done Third") } } } struct FourthView: View { @Environment(\.dismiss) var dismiss @State var isPresenting: Bool = false @State var text: String = "" var body: some View { NavigationStack { VStack(alignment: .leading, spacing: 32) { TextField("Enter text", text: $text) .padding(.top, 20) .padding(.horizontal, 20) Button { isPresenting.toggle() } label: { Text("Present View") } .frame(maxWidth: .infinity, alignment: .center) .padding(.top, 40) Spacer() } .toolbar { toolbarContent } } } @ToolbarContentBuilder var toolbarContent: some ToolbarContent { ToolbarItem(placement: .topBarLeading) { CancelButton { dismiss() } } ToolbarItemGroup(placement: .keyboard) { KeyboardAccessoryDone(title: "Done Forth") } } } public struct KeyboardAccessoryDone: View { // MARK: - Properties private let title: String private let onPress: (() -> ())? // MARK: - Init public init( title: String, onPress: (() -> ())? = nil ) { self.title = title self.onPress = onPress } // MARK: - View public var body: some View { Spacer() Button(title) { if let onPress { /// If the consumer of this view has chosen to inject a completion closure, then call it. onPress() } else { /// If the consumer of this view has omitted to inject a completion closure, then close the keyboard by calling `endEditing`. UIApplication.shared.endEditing() } } } } public struct CancelButton: View { private var onPress: (() -> Void) public init( onPress: @escaping () -> Void ) { self.onPress = onPress } public var body: some View { Button { onPress() } label: { Text("Cancel") } .accessibilityIdentifier("cancelButton") } } extension UIApplication { func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } }
1
0
324
Aug ’24