Code that worked well before upgrading to Xcode 16 doesn't work if it builds on Xcode 16.
What doesn't work will not scroll when you use ScrollViewReader to scrollTo with View assigned id.
The bug is only reproduced on iOS 18 devices, and it works well on IOS 18 devices when built on Xcode 15 version.
Attached is the sample code below.
import SwiftUI
struct RegisterSheetView: View {
@State private var keyword = ""
@State private var descriptionMessage: String?
@State private var sheetHeight: CGFloat = .zero
@State private var sheetFirstPageHeight: CGFloat = .zero
@State private var sheetSecondPageHeight: CGFloat = .zero
@State private var sheetScrollProgress: CGFloat = .zero
var body: some View {
GeometryReader(content: { geometry in
let size = geometry.size
ScrollViewReader(content: { proxy in
ScrollView(.horizontal) {
HStack(alignment: .top, spacing: 0) {
FirstPage(size)
.id("First")
SecondPage(size)
.id("Second")
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
.scrollIndicators(.hidden)
.overlay(alignment: .topTrailing) {
Button(action: {
if sheetScrollProgress < 1 {
// First Page Button Action
withAnimation(.snappy) {
proxy.scrollTo("Second", anchor: .leading)
}
} else {
// Second Page Button Action
print("Register Button Action")
}
}, label: {
Text("continue")
.fontWeight(.semibold)
.opacity(1 - sheetScrollProgress)
.frame(width: 120 + (sheetScrollProgress * 15))
.overlay(content: {
Text("regist")
.fontWeight(.semibold)
.opacity(sheetScrollProgress)
})
.padding(.vertical, 12)
.foregroundStyle(.white)
.background(.linearGradient(colors: [.red, .orange], startPoint: .topLeading, endPoint: .bottomTrailing), in: .capsule)
})
.padding(15)
.offset(y: sheetHeight - 100)
.offset(y: sheetScrollProgress * -90)
}
})
})
.presentationCornerRadius(30)
.presentationDetents(sheetHeight == .zero ? [.medium] : [.height(sheetHeight)])
.onDisappear {
keyword = ""
}
}
// First View
@ViewBuilder
func FirstPage(_ size: CGSize) -> some View {
VStack(alignment: .leading, spacing: 12, content: {
Text("First Page")
.font(.largeTitle.bold())
.lineLimit(2)
Text(attributedSubTitle)
.font(.callout)
.foregroundStyle(.gray)
Text("What's the problem")
.foregroundStyle(.blue)
.font(.system(size: 16, weight: .semibold))
.frame(width: size.width, alignment: .leading)
.onTapGesture {
print("Tapped")
}
})
.padding(15)
.padding(.horizontal, 10)
.padding(.top, 15)
.padding(.bottom, 130)
.frame(width: size.width, alignment: .leading)
.heightChangePreference { height in
sheetFirstPageHeight = height
sheetHeight = height
}
}
var attributedSubTitle: AttributedString {
let string = "It works fine on Xcode 15 but builds on Xcode 16 and doesn't work on iOS 18 devices."
var attString = AttributedString(stringLiteral: string)
if let xcode16Range = attString.range(of: "Xcode 16") {
attString[xcode16Range].foregroundColor = .black
attString[xcode16Range].font = .callout.bold()
}
if let ios18Range = attString.range(of: "iOS 18") {
attString[ios18Range].foregroundColor = .black
attString[ios18Range].font = .callout.bold()
}
return attString
}
// Second View
@ViewBuilder
func SecondPage(_ size: CGSize) -> some View {
VStack(alignment: .leading, spacing: 12) {
Text("Key Registration")
.font(.largeTitle.bold())
CustomTextField(hint: "Please enter the contents", text: $keyword, icon: "person")
.padding(.top, 20)
}
.padding(15)
.padding(.horizontal, 10)
.padding(.top, 15)
.padding(.bottom, 190)
.overlay(alignment: .bottom, content: {
VStack(spacing: 15) {
Text("Apple **[Developer Forums](https://developer.apple.com/forums/)**")
.font(.caption)
.tint(.blue)
.foregroundStyle(.gray)
}
.padding(.bottom, 15)
.padding(.horizontal, 20)
.multilineTextAlignment(.center)
.frame(width: size.width)
})
.frame(width: size.width)
.heightChangePreference { height in
sheetSecondPageHeight = height
let diff = sheetSecondPageHeight - sheetFirstPageHeight
sheetHeight = sheetFirstPageHeight + (diff * sheetScrollProgress)
}
.minXChangePreference { minX in
let diff = sheetSecondPageHeight - sheetFirstPageHeight
let truncatedMinX = min(size.width - minX, size.width)
guard truncatedMinX > 0 else { return }
let progress = truncatedMinX / size.width
sheetScrollProgress = progress
sheetHeight = sheetFirstPageHeight + (diff * progress)
}
}
}
extension View {
@ViewBuilder
func heightChangePreference(completion: @escaping (CGFloat) -> ()) -> some View {
self
.overlay {
GeometryReader(content: { geometry in
Color.clear
.preference(key: SizeKey.self, value: geometry.size.height)
.onPreferenceChange(SizeKey.self) { value in
DispatchQueue.main.async {
completion(value)
}
}
})
}
}
@ViewBuilder
func minXChangePreference(completion: @escaping (CGFloat) -> ()) -> some View {
self
.overlay {
GeometryReader(content: { geometry in
Color.clear
.preference(key: OffsetKey.self, value: geometry.frame(in: .scrollView).minX)
.onPreferenceChange(OffsetKey.self) { value in
DispatchQueue.main.async {
completion(value)
}
}
})
}
}
}
struct SizeKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct OffsetKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct CustomTextField: View {
var hint: String
@Binding var text: String
var icon: String
var body: some View {
VStack(alignment: .leading, spacing: 12) {
TextField(hint, text: $text)
.padding(.trailing, 28)
.padding(.top, 10)
Divider()
}
.overlay(alignment: .trailing) {
Image(systemName: icon)
.foregroundStyle(.gray)
}
}
}
struct ContentView: View {
@State private var isPresented: Bool = false
var body: some View {
Button {
isPresented.toggle()
} label: {
Text("Show Bottom Sheet")
.padding()
.background(Color.yellow)
}
.sheet(isPresented: $isPresented) {
RegisterSheetView()
}
}
}