[quote='818168022, JimMosher, /thread/770846?answerId=818168022#818168022, /profile/JimMosher']
Yes, you are correct, in the Swift is quite a shock and seems to be a large learning curve. And yes, I am trying to find whatever I can to learn this language. Thanks for the links, I will certainly check these out.
[/quote]
Just to clarify: Judging from your question, the problem might be less about the Swift language, itself (which I wager that you will quickly grok) than the SwiftUI framework (which is a framework where the UI, itself, is defined declaratively, coincidentally also in Swift). It is this latter framework for defining user interfaces that might feel a bit alien at first. But I don’t imagine you’ll have any troubles mastering Swift, itself, in general.
For what it is worth, in Apple ecosystems, when writing GUI apps, there are two paradigms for designing user interfaces. The older of the two is a “storyboard”, where you design and edit the UI in the Xcode GUI. This Xcode editor where you design UI’s is known, for historical reasons, as “Interface Builder”. Anyway, when you use storyboards, you then write familiar Swift imperative code to say what you want to do when certain things happen (e.g., the view is loaded, when the user taps a button, etc.). And you hook up the storyboard UI with your imperative code with “outlets” (@IBOutlet) and “actions” (@IBAction). It is the older, tried-and-true pattern.
The more modern way of designing user interfaces is SwiftUI, where, on top of all the more general Swift code you may write for various services, the UI is also defined in Swift. That’s what this ContentView is: It is a declarative definition of a UI, written in Swift.
When you create a new project, you can pick between “Storyboard” or “SwiftUI” for the UI (unless you create a multiplatform app, in which case SwiftUI is the only choice). If learning both Swift and SwiftUI in one fell swoop is too much to take in, you could always create an old-school storyboard-based app, at which point you could focus on familiarizing yourself with the Swift language. And then you could tackle SwiftUI, at your leisure, in a future project. But if you do this, you’ll have to learn a lot of legacy storyboard skills which will be of limited use when you later adopt SwiftUI.
So, feel free to dive into SwiftUI, directly, if you want. Just recognize that you will be learning two completely different techs, one is the Swift language, itself, and the other is SwiftUI’s declarative patterns for designing user interfaces, which happens to be written in Swift, as well. The various tutorials and videos I shared with you earlier will help you get up to speed on the latter.
Post
Replies
Boosts
Views
Activity
[quote='770846021, JimMosher, /thread/770846, /profile/JimMosher']
The most confusing thing for me, is where is "main"?
[/quote]
Let’s say that your app was called Foo. You will see a FooApp file in your project, and that bears the @main qualifier. It will look like:
import SwiftUI
@main
struct FooApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
That’s how it knows to present your ContentView.
Now, you included a screen snapshot where you tried dropping a call to result in various places. That will not work because this is a “declarative” framework, and it is expecting that the body will consist of a hierarchy of View types. But we can add “view modifiers” to any of the View’s within that body.
For example, there is an “event view modifier” called .onAppear, where you can specify what should happen when the associated View appears:
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
.onAppear {
foo()
}
}
func foo() {
print("foo")
}
}
Or, if you wanted to do something asynchronous, we would do that in a .task view modifier:
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
.task {
await bar()
}
}
func bar() async {
do {
try await Task.sleep(for: .seconds(1))
print("bar")
} catch {
print(error)
}
}
}
Note, if this is your first foray into a declarative language, I might suggest going through some of the tutorials on the SwiftUI page. Or perhaps watch some of the SwiftUI videos, such as WWDC 2024’s SwiftUI essentials (or any of the videos linked on that page).
It seems to be a manifestation of the bug outlined at https://forums.developer.apple.com/forums/thread/736226. In my tests, I use an implementation like yours, except I omit the ModelActor protocol conformance, and it seems to work fine.
I agree with Ed, that the “Leaks” instrument is not as illuminating as the “Allocation” instrument. The “Leaks” instrument is most useful (in those edge-cases) where you are writing code that does manual alloc/free or retain/release. If not, it is generally identifying (usually very small) issues largely outside of your control. But if you don’t have your own code that is doing alloc/free or retain/release, the “Leaks” instrument is not terribly illuminating.
Your “Debug Memory Graph” diagnostics, though, are very effective in identifying strong reference cycles. But rather than focusing on the bottom of the tree (the fact that the NSSet was used deeply within Foundation types), I would focus on the top of that tree. E.g., it looks like the NSSet was created by some AVFoundation layer view. So, I would focus on the AVFoundation usage within the app.
Even better, in the left panel of the “Debug Memory Graph”, I would advise that we focus on our own objects, i.e., those objects over which we have control. To the right of the filter bar at the bottom of the panel on the left, there is a button that says “Show only content from workspace”: That’s a great way to focus on those objects over which you have control. See if there are any of your objects present that are still present that should no longer be there. E.g., you might have a view controller that shouldn’t be there (perhaps because of some strong reference cycle); and that view controller may lead to many objects (like this NSSet) not being released, as well. So rather than worrying about the NSSet, focus on the view controller (or which ever of your objects that should no longer be in memory). Bottom line, even though this big object was buried in some Foundation type, it often is a side-effect of some issue in the memory management of our own objects.
So, I might advise a “top down” focus on the objects in memory, rather than “bottom up”. A single top level object from your workspace that hasn’t been released will lead to a cacophony of lower-level objects, and there’s no reason to dwell on those lower-level objects until you’ve resolved your top-level objects that haven’t been released.
One last suggestion: When using “Debug Memory Graph”, sometimes it is not enough to know which type of object is keeping a strong reference to another, but rather you want help knowing where in your code these strong references are established. If you haven’t already, I would go to your scheme’s “Run” » “Diagnostics” options, and temporarily turn on “Malloc Stack Logging”. This way, when looking at the “Debug Memory Graph”, you can show the stack trace on the right, and it can help you navigate to the line of code that established a strong reference. Sometimes this isn’t necessary (as one can make educated guesses), but sometimes it can be incredibly useful if you cannot otherwise divine what you may have done to establish a strong reference cycle, or the like. Just remember to turn off this “Malloc Stack Logging” once you have identified and resolved your memory management issue.
There are two considerations:
The MKLocalSearchCompletion results, much less the MKLocalSearchCompleter, are not Sendable. So, we would map that to something that is. (The term of art is a “data transfer object”.) A tuple is fine, but a custom DTO object (that captures all four properties, but not description) would be best.
The completerDidUpdateResults is a nonisolated requirement. So you probably want to get back to the main actor. If you were not on the main thread, you would use Task { @MainActor in … }. But since this delegate method is called on the main thread, MainActor.assumeIsolated {…} is preferable.
Thus:
extension ViewController: MKLocalSearchCompleterDelegate {
nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
let results = completer.results.map(MKLocalSearchCompletionDTO.init)
MainActor.assumeIsolated {
handleCompleterResults(results)
}
}
}
Where:
struct MKLocalSearchCompletionDTO {
let title: String
let subtitle: String
let titleHighlightRanges: [Range<String.Index>]
let subtitleHighlightRanges: [Range<String.Index>]
init(_ result: MKLocalSearchCompletion) {
title = result.title
subtitle = result.subtitle
titleHighlightRanges = result.titleHighlightRanges
.compactMap { Range($0.rangeValue, in: result.title) }
subtitleHighlightRanges = result.subtitleHighlightRanges
.compactMap { Range($0.rangeValue, in: result.subtitle) }
}
}
Note, one would generally mirror the properties of the original object in the DTO rendition, but I am converting the two highlight ranges of [NSValue] to swiftier [Range<String.Index>] for easier use where the object is consumed. But that is a matter of personal preference.
tl;dr
The Observation framework does not require isolating your @Observable objects to the main actor. Rather than assuming that the lack of a warning message is a bug, we can be pretty confident that the main thread checker is doing its job and no isolation of the main actor is needed.
A few observations:
Yes, when using ObservableObject, WWDC videos repeatedly advised isolating this to the main actor, to make sure that UI updates happen on the main thread.
You are correct that with @Observable with SwiftUI, the WWDC videos and official documentation lack formal assurances that explicit isolation to the main actor is no longer needed, but experience has confirmed this to be the case. But, they certainly haven’t warned us that we should isolate it to the main actor, like they did with ObservableObject.
While I agree that greater clarity would be appreciated, I do not share your opinion that this means that this means that “it could lead to crashes” nor that it is an unaddressed “pretty major bug”. It feels like a deficiency in the documentation, more than anything else.
To use your turn of phrase, I think this is a “non issue”: With SwiftUI, you do not have to isolate the @Observable to the main actor. (You may want to for other reasons; see point 6, below.)
For what it is worth, while Apple’s documentation is sorely silent on this point, the fact that main actor isolation is not required has been discussed by others (e.g., https://www.sobyte.net/post/2023-08/observation-framework).
As an aside, you said:
I see a lot of posts about this from around the initial release of Async/Await talking about using await MainActor.run {} at the point the state variable is updated.
Yes, there were a lot of those. I think those stemmed from developers who were used to the DispatchQueue.main.async {…} pattern.
But, IMHO, that is code smell. If the property is properly isolated to the main actor, this isn’t needed. In fact, if you see WWDC 2019 Swift concurrency: Update a sample app, they demonstrate the transition from GCD to MainActor.run {…} and finally to just isolate to the correct actor, rendering MainActor.run unnecessary.
Also a bit tangential to the question at hand, but you said:
On some ways similar to the fact that many of the early posts I have seen related to @Observable have examples of an @Observable ViewModel instantiated in the view as an @State variable, but in fact this is not needed as that is addressed behind the scenes for all properties of an @Observable type.
I am not sure if I follow you. If you have an @Observable class, you would generally declare that as a @State property in the View. (Yes, we don’t need @StateObject any more, a view generally would store the view model in a @State property.)
You said:
Also, Thread.current is unavailable in asynchronous contexts, so says the warning. And I have read that in a sense you simply aren't concerned with what thread an async task is on.
The retirement of Thread.current has nothing to do with this current topic. It is not available from Swift concurrency because it could lead one to draw incorrect conclusions. If you really care about the threading model underpinning Swift concurrency, I would suggest watching WWDC 2019 Swift concurrency: Behind the scenes. But just because the Thread API is not available from Swift concurrency, it does not mean that we do not care about making sure we have the right actor isolation. It just means that we should not dwell on threads, per se.
Now, let’s come back to the @Observable object that has some Task updating some observed property. As soon as you change the “Strict concurrency checking” build setting to “Complete” and/or adopt Swift 6, you will quickly realize that the compiler will complain if it is unable to reason about your object’s thread-safety. (And please, avoid the @unchecked Sendable trick to silence really meaningful warnings; only use that if you cannot use actors or value types to ensure thread safety and have, instead, used some legacy synchronization mechanism to manually implement thread-safety.) Using actor-isolation in your @Observable object is the modern and easiest way to achieve thread-safety for mutable types.
This is a long winded way of saying that just because @Observable doesn’t require isolation to the main actor, that you might not choose to do so, anyway. You can isolate your @Observable class to any global actor for reasons of thread-safety, and often the main actor is a fine choice.
So, if you really are afraid that the absence of warnings stems from a bug in the main thread checker and that this might cause problems (a concern I do not share), then just isolate this @Observable object to the main actor and call it a day. You will generally want to isolate it to a global actor, anyway, and the main actor is generally an adequate solution (as long as you never do anything slow and synchronous directly from the main actor). Personally, for an object driving the UI, I would generally isolate it to the main actor for thread-safety reasons, anyway.
To avoid duplicative threads, I would direct people to your other question.
You said:
It also seems like having multiple ModelActors, as in this case (one for observation and one for data changes), causes interference and instability.
Yeah, having multiple ModelActors seems like an invitation for problems. I use a single ModelActor for all interaction with the data store and I don’t have any of these problems. And if doing anything very slow (like inserting thousands of records), just add an await Task.yield() in the loop and that will keep it responsive.
Note, there is a bug in ModelActor which runs it on the main queue (!), so I avoid the @ModelActor macro and just define my own actor:
actor FooModelActor {
private let modelExecutor: any ModelExecutor
private let modelContainer: ModelContainer
private let modelContext: ModelContext
init(inMemoryOnly: Bool = false) {
let schema = Schema(…)
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: inMemoryOnly)
modelContainer = try! ModelContainer(for: schema, configurations: [modelConfiguration])
modelContext = ModelContext(modelContainer)
modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext)
}
…
}
Basically, I expanded the @ModelActor macro and grabbed the salient properties, but avoided the portions that introduced this main thread problem.
I see the same problem whenever I profile on a physical iOS device from Xcode 16.1 or 16.2 Beta 2 with any instrument using a recording mode of “deferred” (which happens to be the default recording mode for the Hitches tool). If I change the recording mode to “Capture last n seconds” (or, when available, “Immediate”), the errors do not appear.
FWIW, my configuration is similar: M1 Mac Studio, Sequoia 15.1. Profiling a new blank project on iPhone 16 Pro Max, iOS 18.1.
For what it’s worth, I get around this with a rendition that doesn’t take a delegate parameter, cutting the Gordian knot:
extension URLSession {
public nonisolated func download(with url: URL) async throws -> (URL, URLResponse) {
try await download(from: url)
}
}
it's as buggy as usual
Yeah, I had problem with app store install, too. How is this still a thing?
Then you can just do the appropriate os_signpost calls. Personally, I use a helper swift class to make sure that the category is correct and the format of the strings is correct. E.g.
import Foundation
import os.log
// MARK: - CustomPointsOfInterestLog
/// Custom Points of Interest Log
///
/// This allows logging of events and intervals to a custom “Points of Interest” tool in Instruments.
///
/// Needless to say, this assumes that you have installed the custom Points of Interest tool in Instrumewnts.
class CustomPointsOfInterestLog {
fileprivate let log: OSLog
init(subsystem: String) {
log = OSLog(subsystem: subsystem, category: "Interval")
}
func event(name: StaticString = "Points", label: String, concept: EventConcept = .debug) {
os_signpost(.event, log: log, name: name, InstrumentsInterval.formatString, label, concept.rawValue)
}
func interval<T>(name: StaticString = "Intervals", label: String, concept: EventConcept = .debug, block: () throws -> T) rethrows -> T {
let interval = InstrumentsInterval(name: name, label: label, concept: concept, log: self)
interval.begin()
defer { interval.end() }
return try block()
}
}
// MARK: - EventConcept
extension CustomPointsOfInterestLog {
/// EventConcept enumeration
///
/// This is used to dictate the color of the intervals in our custom instrument.
/// See [Event Concept Engineering Type](https://help.apple.com/instruments/developer/mac/current/#/dev66257045).
enum EventConcept: String {
case success = "Success"
case failure = "Failure"
case fault = "Fault"
case critical = "Critical"
case error = "Error"
case debug = "Debug"
case pedantic = "Pedantic"
case info = "Info"
case signpost = "Signpost"
case veryLow = "Very Low"
case low = "Low"
case moderate = "Moderate"
case high = "High"
case red = "Red"
case orange = "Orange"
case blue = "Blue"
case purple = "Purple"
case green = "Green"
}
}
// MARK: - InstrumentsInterval
/// Interval to be shown in custom instrument when profiling app
struct InstrumentsInterval {
fileprivate static let formatString: StaticString = "Label:%{public}@,Concept:%{public}@"
let name: StaticString
let label: String
let concept: CustomPointsOfInterestLog.EventConcept
let log: CustomPointsOfInterestLog
let id: OSSignpostID
init(name: StaticString, label: String, concept: CustomPointsOfInterestLog.EventConcept = .debug, log: CustomPointsOfInterestLog) {
self.name = name
self.concept = concept
self.label = label
self.log = log
self.id = OSSignpostID(log: log.log)
}
/// Manually begin an interval
func begin() {
os_signpost(.begin, log: log.log, name: name, signpostID: id, Self.formatString, label, concept.rawValue)
}
/// Manually end an interval
func end() {
os_signpost(.end, log: log.log, name: name, signpostID: id)
}
/// Manually emit an event
func event() {
os_signpost(.event, log: log.log, name: name, signpostID: id, Self.formatString, label, concept.rawValue)
}
}
And then I can do logging like so:
private let log = CustomPointsOfInterestLog(subsystem: "Example")
class Example {
func synchronousTask() {
log.interval(name: "Example", label: #function, concept: .green) {
Thread.sleep(forTimeInterval: 2)
}
}
}
And I can see my colored output:
I color my points of interest using a custom instrument
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Instruments Developer Help: https://help.apple.com/instruments/developer/mac/current/ -->
<package>
<id>com.robertmryan.CustomInterval</id>
<version>0.2</version>
<title>Custom Points of Interest</title>
<owner>
<name>Robert Ryan</name>
</owner>
<import-schema>os-signpost</import-schema>
<!-- See https://help.apple.com/instruments/developer/mac/current/#/dev536412616 -->
<os-signpost-point-schema>
<id>custom-point-schema</id>
<title>Points</title>
<owner>
<name>Robert Ryan</name>
</owner>
<purpose>Provide mechanism for multicolored events posted by `os_signpost`; The string generated by `os_signpost` must be in form of "Label:%d,Concept:%{public}@", where "Label" is string that will control what text appears in the event, and "Concept" is one of the strings listed in https://help.apple.com/instruments/developer/mac/current/#/dev66257045 that dictates the color of the interval. No spaces after the commas within this string.</purpose>
<note>That message must use that printf-style format, not embedding the values in the format string literal.</note>
<!-- you can constrain this to a particular subsystem if you'd like:
<subsystem>"com.domain.MyApp"</subsystem>
-->
<category>"Interval"</category>
<name>?name</name>
<pattern>
<message>"Label:" ?label ",Concept:" ?concept</message>
</pattern>
<column>
<mnemonic>name</mnemonic>
<title>Name</title>
<type>string</type>
<expression>?name</expression>
</column>
<column>
<mnemonic>label</mnemonic>
<title>Label</title>
<type>string</type>
<expression>?label</expression>
</column>
<column>
<mnemonic>concept</mnemonic>
<title>Concept</title>
<type>event-concept</type>
<expression>?concept</expression>
</column>
</os-signpost-point-schema>
<os-signpost-interval-schema>
<id>custom-interval-schema</id>
<title>Intervals</title>
<owner>
<name>Robert Ryan</name>
</owner>
<purpose>Provide mechanism for multicolored intervals posted by `os_signpost`; The string generated by `os_signpost` must be in form of "Label:%d,Concept:%{public}@", where "Label" is string that will control what text appears in the interval, and "Concept" is one of the strings listed in https://help.apple.com/instruments/developer/mac/current/#/dev66257045 that dictates the color of the interval. No spaces after the commas within this string.</purpose>
<note>That message must use that printf-style format, not embedding the values in the format string literal.</note>
<!-- you can constrain this to a particular subsystem if you'd like:
<subsystem>"com.domain.MyApp"</subsystem>
-->
<category>"Interval"</category>
<name>?name</name>
<start-pattern>
<message>"Label:" ?label ",Concept:" ?concept</message>
</start-pattern>
<column>
<mnemonic>name</mnemonic>
<title>Name</title>
<type>string</type>
<expression>?name</expression>
</column>
<column>
<mnemonic>label</mnemonic>
<title>Label</title>
<type>string</type>
<expression>?label</expression>
</column>
<column>
<mnemonic>concept</mnemonic>
<title>Concept</title>
<type>event-concept</type>
<expression>?concept</expression>
</column>
</os-signpost-interval-schema>
<instrument>
<id>com.robertmryan.CustomInterval.instrument</id>
<title>Custom Points of Interest</title>
<category>Behavior</category>
<purpose>Provide multi-colored intervals as dictated by the "event-concept" parsed from the `start-pattern` string.</purpose>
<icon>Points of Interest</icon>
<limitations></limitations>
<create-table>
<id>custom-interval-table</id>
<schema-ref>custom-interval-schema</schema-ref>
</create-table>
<create-table>
<id>custom-point-table</id>
<schema-ref>custom-point-schema</schema-ref>
</create-table>
<graph>
<title>Custom Interval Graph</title>
<lane>
<title>Points</title>
<table-ref>custom-point-table</table-ref>
<plot-template>
<instance-by>name</instance-by>
<label-format>%s</label-format>
<value-from>name</value-from>
<color-from>concept</color-from>
<label-from>label</label-from>
</plot-template>
</lane>
<lane>
<title>Intervals</title>
<table-ref>custom-interval-table</table-ref>
<plot-template>
<instance-by>name</instance-by>
<label-format>%s</label-format>
<value-from>name</value-from>
<color-from>concept</color-from>
<label-from>label</label-from>
<qualified-by>layout-qualifier</qualified-by>
</plot-template>
</lane>
</graph>
<list>
<title>Custom Regions of Interest</title>
<table-ref>custom-interval-table</table-ref>
<column>name</column>
<column>label</column>
<column>concept</column>
<column>start</column>
<column>duration</column>
</list>
<list>
<title>Custom Points of Interest</title>
<table-ref>custom-point-table</table-ref>
<column>name</column>
<column>label</column>
<column>concept</column>
</list>
</instrument>
</package>
As carlos_reyes - https://developer.apple.com/forums/profile/carlos_reyes said:
I had the same issue. I restarted the MacBook in safe mode (press and hold the Shift key while it boots) and I was able to perform the update. I hope this will work for you. Worked for me.