Post

Replies

Boosts

Views

Activity

Reply to Observation and MainActor
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() } } }
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Nov ’23
Reply to How to avoid Swift 6 concurrency warning from UIAccessibility.post()
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.
Topic: Programming Languages SubTopic: Swift Tags:
Jun ’24
Reply to How to build a top/bottom split view with a dynamically-sized divider?
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() }
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Oct ’24
Reply to Simple SwiftData app exhibits excessive & persistent memory growth as items are added
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.
Nov ’24
Reply to Product Page Optimization regression: App Icon tab no longer available
UPDATE: Without me doing anything, the App Icon tab showed up today. Not sure what caused the problem, but I did notice App Store Connect had performance issues in its system status, so perhaps some maintenance was going on earlier that caused the tabs disappearance. 🤷 Either way, I'm glad I was finally able to submit my optimization test.
Jun ’25
Reply to Verifying braille output in an iOS app without a physical braille device?
Okay, after more research and reading these articles: Use a braille display with VoiceOver on iPhone Change your VoiceOver settings on iPhone It sounds like braille displays just use the VoiceOver output and whether it "reads" parentheses is part of the device's Verbosity › Punctuation setting. Then looking into whether I can set that programmatically, I came across this .speechAlwaysIncludesPuncturation() modifier, which may do exactly what I want. I'll give that a try report back! 🤞
Jun ’25
Reply to Verifying braille output in an iOS app without a physical braille device?
The only workaround I could find requires people to create a custom punctuation group that includes only mathematical symbols, then manually switch to it when using my app. This feels like a significant burden and seems at odds with providing a seamless experience. This highlights what feels like a gap in the accessibility API: no way for developers to precisely control VoiceOver speech for technical content (pronounce specific symbols but ensure natural number reading) while simultaneously ensuring literal display on Braille devices. I'm hoping an Apple accessibility engineer or experienced developer might know a better approach than requiring users to modify system-level settings for one app.
Jun ’25
Reply to VoiceOver and currency - high amounts
A similar thing happens with six or more decimal digits. For example: Text("0.12345") -> "zero point one two three four five" But this: Text("0.123456") -> "zero one, two, three, four, five, six" (commas represent pauses) How can I force VoiceOver to announce the decimal separator ("point") and not insert pauses regardless of the number of decimal digits?
Jun ’25