In a UIKit context, has anyone had experience/success in using async/await to synchronize a modal dialog with other logic? I've tried it a bit without success.
I.e, given a presented dialog, I want to capture data in the dialog, then use the results in a simple, linear fashion. (Something that looks like "Present the dialog, wait for results, use results" -- all inline without closures.)
It seems to me that async/await with @MainActor ought to make that possible, but I haven't yet figured out how. I'd really like to see a real-world example.
Can you help?
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Xcode 13.1, iOS 25 SDK
I have a UICollectionView (with a custom UICollectionViewLayout) which I am updating with a DiffableDataSource. This CollectionView displays a particular data object (with a given number of sections and items).
The user can choose to display a different object (with a different number of sections and items), reusing this CollectionView. When this occurs, I do the following:
(pass the new data object to the custom layout)
let layout = collectionView.collectionViewLayout
layout.invalidateLayout()
collectionView.contentSize = layout.collectionViewContentSize
(calculate a new snapshot and apply it)
When the new data object has FEWER sections and/or items as the first, I get a series of warning messages (one for each of the cells that are no longer in the collection view) as follows:
2021-12-07 14:29:02.239301-0600 WineCorner[82916:1921357] [CollectionView] Layout attributes <UICollectionViewLayoutAttributes: 0x7f77b3047e00> index path: (<NSIndexPath: 0xa0f0b1eb018e754a> {length = 2, path = 0 - 4}); frame = (578.118 6; 160 160); transform = [0.70710678118654757, -0.70710678118654746, 0.70710678118654746, 0.70710678118654757, 0, 0]; zIndex = 1; were received from the layout <WineCorner.WineRackLayout: 0x7f77b1f1a0c0> but are not valid for the data source counts. Attributes will be ignored.
Obviously, I am not handling the situation correctly. What should I do so that these warning messages are not issued?
I am upgrading an existing (CoreData-based) iOS app to support CloudKit synchronization. This doesn't work because the app includes an ORDERED relationship.
I need to perform a migration from this ordered relationship to something else that supports both CloudKit and maintains ordering.
The problem is less about WHAT I need to do to support my requirements as it is HOW to perform the migration (so that my existing customers don't lose data.) Have you encountered this problem? How did you solve it?
FB13099793
I have a view which lists detail information about a SwiftData object, including a list of the members of its relationship.
The Problem:
If I use a modal presentation to edit the content of a relationship-member, upon return, the member has been updated, but the view falis to show it. The view updates correctly on adding a new member to the relationship, deleting a member, etc. But I cannot get the List to update if the content of the member changes (in a modal presentation).
A few requirements that may be of significance:
(1) The relationship (and inverse) are defined as optional because it is an eventual requirement for this app to use CloudKit synchronization.
(2) The display of the members must be ordered. For this reason, the member object contains a "ordinal" property which is used to sort the the members.
The relevant parts of the models are:
@Model
final public class Build {
public var bld: Int
@Relationship(inverse: \ChecklistItem.build)
public var checklist: [ChecklistItem]?
public init(bld: Int) {
self.bld = bld
self.checklist = []
}
}
@Model
final public class ChecklistItem {
public var ordinal: Int
public var title: String
public var subtitle: String
// etc.
public var build: Build?
public init(build: Build) {
self.ordinal = -1
self.title = ""
self.subtitle = ""
self.build = build
}
}
The relevant parts of the view which handles the display is shown below. (Please look at the notes that follow the code for a discussion of some issues.)
struct buildDetailChecklistView: View {
@Environment(\.modelContext) var context: modelContext
@State private var selectedItem: ChecklistItem? = nil
@Bindable var build: Build
init(build: Build) {
self.build = Build
}
var body: some View {
VStack {
// ... some other stuff
}
List {
ForEach((build.checklist ?? [])
.sorted(by: { (a,b) in a.ordinal < b.ordinal})) { item in
RowView(item) // displays title, subtitle, etc.
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
deleteRow(item)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
.swipeActions(edge: .leading, allowsFullSwipe: false) {
Button {
selectedItem = item
} label: {
Label("Edit", systemImage: "pencil.line")
}
.tint(.blue)
}
}
.sheet(item: $selectedItem) { item in
BuildDetailAddEditChecklistItem(item: item,
handler: updateChecklist(_:))
}
}
}
private func updateChecklist(_ item: ChecklistItem) {
if let index = build.checklist!.firstIndex(where: { $0 == item }) {
DispatchQueue.main.async { [index] in
build.checklist!.remove(at: index)
try? context.save()
build.checklist!.insert(item, at: index)
}
}
}
}
Notes:
(1) I cannot use a @Query macro in this case because of limitations in #Predicate. Every predicate I tried (to match the ChecklistItem's build member with it's parent's build object crash.)
(2) I don't want to use @Query anyway because there is no need for the extra fetch operation it implies. All of the data is already present in the relationship. There ought to be a new macro/propertyWrapper to handle this.
(3) Dealing with the required sort operation on the relationship members [in the ForEach call] is very awkward. There ought to be a better way.
(4) The BuildDetailAddEditChecklistItem() function is a modal dialog, used to edit the content of the specified ChecklistItem (for example, changing its subtitle). On return, I expected to see the List display the new contents of the selected item. IT DOES NOT.
(5) The handler argument of BuildDetailAddEditChecklist() is one of the things I tried to "get the List's attention". The handler function is called on a successful return from the model dialog. The implementation of the handler function finds the selected item in the checklist, removes it, and inserts it back into the checklist. I expected that this would force an update, but it does not.
I have a SwiftData Model which includes a property of type Array.
@Model
final class Handler {
public var id: UUID
public var name: String
public var mailingAddress: [String]
. . .
public init() {
self.id = UUID()
self.name = ""
self.mailingAddress = []
. . .
}
When I use this Model in code, the mailingAddress field appears to work just as I would expect, but when saving it (running in Simulator), I see the following logged message:
CoreData: fault: Could not materialize Objective-C class named "Array" from declared attribute value type "Array<String>" of attribute named mailingAddress
Should I be worried about this?
I've found another interesting issue with the Focus system.
My text case is SwiftUI in Preview or Simulator (using an iPad mini case) -- I haven't tried this on other environments, so your mileage may vary.
I have a Form including several TextFields. Adding the usual @FocusState logic, I observe that if I type a TAB key, then
(a) If the TextFields are specified using TextField(“label”, text: $text), typing the tab key switches focus from field to field as expected, BUT
(b) If the TextFields are specified using TextField(“label”, text: $text, axis: .vertical), typing the tab key adds whitespace to the field and does NOT change focus.
In the latter case, I need to use an .onKeyPress(.tab) {….} modifier to achieve the desired result.
Reported as FB15432840
While I've seen examples of using $FocusState with Lists containing "raw" TextFields, in my use case, the List rows are more complex than that and contain multiiple elements, including TextFields. I obviously don't understand something fundamental here, because I am completely unable to get TextField-to-TextField tabbing to work. Can someone set me straight?
Sample code demonstrating the issues:
//
// ContentView.swift
// ListElementFocus
//
// Created by Richard Aurbach on 10/12/24.
//
import SwiftUI
/// NOTE: in my actual app, the data model is actually a set of SwiftData
/// PresistentModel objects. Here, I'm simulating them with an Observable.
@Observable
final class TestModel: Identifiable {
public var id: UUID
public var checked: Bool = false
public var title: String = "Test"
public var subtitle: String = "Subtitle"
init(checked: Bool = false, title: String, subtitle: String) {
self.id = UUID()
self.checked = checked
self.title = title
self.subtitle = subtitle
}
}
struct ContentView: View {
/// Instead of a @Query...
@State var records: [TestModel] = [
TestModel(title: "First title", subtitle: "blah, blah, blah"),
TestModel(title: "Second title", subtitle: "more nonsense"),
TestModel(title: "Third title", subtitle: "even more nonsense"),
]
@FocusState var focus: UUID?
var body: some View {
Form {
Section {
HStack(alignment: .top) {
Text("Goal:").font(.headline)
Text(
"If a user taps in the TextField in any row, they should be able to tab from row to row using any keyboard which supports a tab key."
)
}
HStack(alignment: .top) {
Text("#1:").font(.headline)
Text(
"While I will admit that this code is probaby total garbage, I haven't been able to find any way to make tabbing from row to row to work at all."
)
}
HStack(alignment: .top) {
Text("#2:").font(.headline)
Text(
"Tapping the checkbox button causes the row to flash with the current accent color, and I can't find any way to turn that off."
)
}
} header: {
Text("Problems").font(.title3).bold()
}.headerProminence(.increased)
Section {
List(records) { record in
ListRow(record: record, focus: focus)
.onKeyPress(.tab) {
focus = next(record)
return .handled
}
}
} header: {
Text("Example: Selector of Editable Items").font(.title3).bold()
}.headerProminence(.increased)
}
.padding()
}
private func next(_ record: TestModel) -> UUID {
guard !records.isEmpty else { return UUID() }
if record.id == records.last!.id { return records.first!.id }
if let index = records.firstIndex(where: { $0.id == record.id }) {
return records[index + 1].id
}
return UUID()
}
}
struct ListRow: View {
@Bindable var record: TestModel
var focus: UUID?
@FocusState var focusState: Bool
init(record: TestModel, focus: UUID?) {
self.record = record
self.focus = focus
self.focusState = focus == record.id
}
var body: some View {
HStack(alignment: .top) {
Button {
record.checked.toggle()
} label: {
record.checked
? Image(systemName: "checkmark.square.fill")
: Image(systemName: "square")
}.font(.title2).focusable(false)
VStack(alignment: .leading) {
TextField("title", text: $record.title).font(.headline)
.focused($focusState)
Text("subtitle").italic()
}
}
}
}
#Preview {
ContentView()
}
I am trying to do a bit of fancy navigation in SwiftUI using NavigationPath and am having a problem.
I have a root view with includes a button:
struct ClassListScreen: View {
@Bindable private var router = AppRouter.shared
@State private var addCourse: Bool = false
...
var body: some View {
...
Button("Add Class") {
router.currentPath.append(addCourse)
}.buttonStyle(.borderedProminent)
...
.navigationDestination(for: Bool.self){ _ in
ClassAddDialog { course in
sortCourses()
}
}
}
}
router.currentPath is the NavigationPath associated with the operative NavigationStack. (This app has a TabView and each Tab has its own NavigationStack and NavigationPath).
Tapping the button correctly opens the ClassAddDialog.
In ClassAddDialog is another button:
struct ClassAddDialog: View {
@Bindable private var router = AppRouter.shared
@State private var idString: String = ""
...
var body: some View {
...
Button("Save") {
let course = ...
... (save logic)
idString = course.id.uuidString
var path = router.currentPath
path.removeLast()
path.append(idString)
router.currentPath = path
}.buttonStyle(.borderedProminent)
...
.navigationDestination(for: String.self) { str in
if let id = UUID(uuidString: str),
let course = Course.findByID(id, with: context) {
ClassDetailScreen(course: course)
}
}
}
}
My intent here is that tapping the Save button in ClassAddDialog would pop that view and move directly to the ClassDetailScreen (without returning to the root ClassListScreen).
The problem is that the code inside the navigationDestination is NEVER hit. (I.e., a breakpoint on the if let ... statement) never fires. I just end up on a (nearly) blank view with a warning triangle icon in its center. (And yes, the back button takes me to the root, so the ClassAddDialog WAS removed as expected.)
And I don't understand why.
Can anyone share any insight here?