Now that @Observable macro has been out a little bit, what's the latest thinking on this? 🤔
How would one apply @MainActor to the ViewModel of the following code? Currently, doing so gives the "Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context" error on the @State line in the TestApp struct.
import SwiftUI
@main
struct TestApp: App {
@State private var vm = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environment(vm)
}
}
}
@Observable
@MainActor
class ViewModel {
var showDetails: Bool = false
}
struct ContentView: View {
@Environment(ViewModel.self) var vm
var body: some View {
@Bindable var vm = vm
VStack {
DetailsButton(showDetails: $vm.showDetails)
if vm.showDetails {
Text("This is my message!")
}
}
}
}
struct DetailsButton: View {
@Binding var showDetails: Bool
var body: some View {
Button("\(showDetails ? "Hide" : "Show") Details") {
showDetails.toggle()
}
}
}
Post
Replies
Boosts
Views
Activity
I have this issue with HomeDepot.com.
See FB13694084.
This is still a problem with Chrome.
macOS 14.5 (23F79)
Chrome 125.0.6422.113 (Official Build) (arm64)
Thanks for the quick reply and good to know it's not me! (it usually is 😆)
I'm importing SwiftData and SwiftUI, so I tried @preconcurrency import SwiftUI and it does suppress the warning. But I'm new at this and I'm not sure if that's OK or if I should be importing UIKit instead?
Code seems to work so I'm guessing there's a lot of overlap, but I'd like to avoid any unintended side effects down the road.
I should have added…
Xcode version: 15.4 (15F31d)
iOS target: 17.5
Here's an animated GIF of the issue:
I submitted FB15622659 to report the typo in the initial CoreSVG log message ("variabe" should be "variable").
I finally figured it out—this code achieves the behavior I was looking for:
import SwiftUI
enum SnapPoint: CGFloat, CaseIterable {
case top = 0.0
case partial = 0.33
case bottom = 1.0
var value: CGFloat {
return self.rawValue
}
static let allValues: [CGFloat] = SnapPoint.allCases.map { $0.rawValue }
}
struct ContentView: View {
@State private var totalHeight: CGFloat = 0
@State private var topHeight: CGFloat = 0
@State private var dividerHeight: CGFloat = 0
@State private var showMore: Bool = true
@State private var currentSnapPoint: SnapPoint = .partial
@State private var previousDividerHeight: CGFloat = 0
let snapPoints: [SnapPoint] = SnapPoint.allCases
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
TopView()
.frame(maxWidth: .infinity)
.frame(height: max(0, topHeight))
.background(Color.red.opacity(0.3))
.border(.pink)
.clipped()
DividerView(showMore: $showMore)
.zIndex(1)
.background(
GeometryReader { dividerGeometry in
Color.clear
.onAppear {
dividerHeight = dividerGeometry.size.height
if totalHeight == 0 {
totalHeight = geometry.size.height
topHeight = calculate(.partial)
}
previousDividerHeight = dividerHeight
}
.onChange(of: dividerGeometry.size.height) {
withAnimation(.snappy(duration: 0.2)) {
let deltaHeight = dividerGeometry.size.height - previousDividerHeight
previousDividerHeight = dividerGeometry.size.height
dividerHeight = dividerGeometry.size.height
if currentSnapPoint != .top {
topHeight = max(0, topHeight - deltaHeight)
}
if totalHeight == 0 {
totalHeight = geometry.size.height
topHeight = (totalHeight - dividerHeight) / 2
}
}
}
}
)
.gesture(
DragGesture()
.onChanged { value in
topHeight = calculateDraggedTopHeight(value.translation.height)
}
.onEnded { _ in
withAnimation(.snappy(duration: 0.2)) {
let (snapPoint, height) = nearestSnapPoint(for: topHeight)
topHeight = height
currentSnapPoint = snapPoint
}
}
)
BottomView()
.frame(maxWidth: .infinity)
.frame(height: max(0, geometry.size.height - topHeight - dividerHeight))
.background(Color.green.opacity(0.3))
.border(.pink)
.clipped()
}
.onChange(of: geometry.size.height) {
totalHeight = geometry.size.height
topHeight = min(topHeight, totalHeight - dividerHeight)
}
}
}
func calculateDraggedTopHeight(_ translation: CGFloat) -> CGFloat {
return max(0, min(topHeight + translation, totalHeight - dividerHeight))
}
func nearestSnapPoint(for height: CGFloat) -> (SnapPoint, CGFloat) {
let calculatedPoints = snapPoints.map { ($0, calculate($0)) }
let nearest = calculatedPoints.min(by: { abs($0.1 - height) < abs($1.1 - height) }) ?? (.partial, height)
return nearest
}
func calculate(_ point: SnapPoint) -> CGFloat {
switch point {
case .top:
return 0
case .partial:
return (totalHeight * point.value) - dividerHeight
case .bottom:
return totalHeight - dividerHeight
}
}
}
struct DividerView: View {
@Binding var showMore: Bool
var body: some View {
VStack(spacing: 0) {
Text(showMore ? "Tap to hide 'More'" : "Tap to show 'More'")
.padding(16)
.multilineTextAlignment(.center)
if showMore {
Text("More")
.padding(16)
}
}
.frame(maxWidth: .infinity)
.background(Color(.systemBackground))
.onTapGesture {
withAnimation(.snappy(duration: 0.2)) {
showMore.toggle()
}
}
}
}
struct TopView: View {
var body: some View {
Text("Top")
}
}
struct BottomView: View {
var body: some View {
Text("Bottom")
}
}
#Preview {
ContentView()
}
Just wanted to close the loop on this…I ended up submitting a dev support ticket and was told that this issue was fixed in iOS 18 (beta at the time).
After testing on iOS 18, I confirmed that they did address the lag issue. The good news: the SwiftData version remains responsive, even with 3,200 items. The bad news: it still uses twice the memory as the CoreData version.
So I've decided to stay with CoreData in my app until there’s a compelling reason to switch back.