What is the expected thread on which a .collect operation in Swift's Combine will emit?
Specifically I am seeing this code crash in the second precondition, but not the first:
return Publishers.MergeMany(publishers)
.handleEvents(receiveOutput: { _ in
precondition(Thread.isMainThread) // <- This is fine
})
.collect()
.handleEvents(receiveOutput: { _ in
precondition(Thread.isMainThread) // <- Crash is here
})
It is as if the .collect operation is picking a different thread to use, even though the final publisher in the MergeMany must have emitted on the main thread. I have deduced this behavior via crash logs uploaded from Firebase. Can anyone explain this observed behavior?
Post
Replies
Boosts
Views
Activity
I have discovered that the onAppear method of views inside of a SwiftUI list is called inconsistently, based on the presence of a listStyle. The onAppear method is called 100% of the time when there is no listStyle applied, but it is called irregularly when there is a listStyle applied.
Here is demo code:
struct TextRow: View {
@State private var didAppear: Bool = false
private let title: String
init(title: String) {
self.title = title
}
var rowTitle: String {
if didAppear {
return title + " (didAppear)"
} else {
return title
}
}
var body: some View {
Text(rowTitle)
.onAppear {
didAppear = true
}
}
}
struct Section: Hashable {
let title: String
let rows: [String]
}
struct ContentView: View {
var content: [Section] {
var rows = [String]()
for i in 0..<20 {
rows.append("Row \(i)")
}
let section1 = Section(title: "Section 1", rows: rows)
var rows2 = [String]()
for i in 0..<20 {
rows2.append("Row \(i)")
}
let section2 = Section(title: "Section 2", rows: rows2)
return [section1, section2]
}
var body: some View {
List {
ForEach(content, id: \.self) { section in
Text(section.title)
ForEach(section.rows, id: \.self) { row in
TextRow(title: row)
}
}
}
// .listStyle(.grouped) // <-- this will trigger different behavior
.navigationBarTitle("Countries")
.padding()
}
}
Is this expected?
Here is the bad behavior:
Here is the proper behavior (no list style):
For the past few days, all of our in-app purchase product ids are being reported as invalid by the Apple sandbox.
Checking the system status, everything is green: https://developer.apple.com/system-status/
Is there any workaround here?
I'm trying to debug an issue where, after the update of a @Published value on an @ObservedObject in a LazyVStack, the cells don't appear to redraw.
Imagine I have this app below, that draws 18 cells, each representing a different country on a continent (apologize for the ugly colors):
I have a an array of Country objects that I render as cells onscreen. Each Country conforms to Identifiable, and uses a monotonically increasing number to implement the Identifiable protocol.
My question is this: imagine this @ObservedObject, or, in this demo code below, @State, changes somehow. What is returned is a list of identical size, where every Model object returns a monotonically increasing ID. How does the LazyVStack know that it should redraw? I wrote the code below and I feel that it should not work, yet somehow it does. When I click on the "Asia" button, or I click on the "Africa" button, it renders the way I expect and immediately updates the cells -- yet I feel that clicking on the opposite button should have no effects, because it will be loading a collection of the same size, with the same IDs, and so SwiftUI should effectively re-use the old cells.
That scenario I am expecting is the bug that I believe, but cannot yet prove, my actual app is experiencing. I am attempting to write demo code to prove this theory.
Here is the complete demo code that I believe should reproduce this issue (but somehow is working fine).
import SwiftUI
public struct Continent {
public let name: String
public let countries: [Country]
public init(name: String, countries: [Country]) {
self.name = name
self.countries = countries
}
}
extension Continent: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(countries)
}
}
public struct Country: Identifiable, Equatable {
public var id: Int {
return index
}
public let regionCode: String
public let index: Int
public init(regionCode: String, index: Int = 0) {
self.regionCode = regionCode
self.index = index
}
public var countryName: String? {
Locale.current.localizedString(forRegionCode: regionCode)
}
}
extension Country: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(index)
}
}
struct ContentView: View {
@State var continents: [Continent] = continentListAfrica
@ViewBuilder private func Grid(stack: Continent, columns: Int) -> some View {
let chunks = stride(from: 0, to: stack.countries.count, by: columns).map {
Array(stack.countries[$0..<min($0 + columns, stack.countries.count)])
}
Text(stack.name)
ForEach(chunks, id: \.self) { chunk in
GridRow(chunk: chunk, stack: stack, columns: columns)
}
}
@ViewBuilder private func GridRow(chunk: [Country], stack: Continent, columns: Int) -> some View {
let emptyElements = columns - chunk.count
HStack(spacing: 5) {
ForEach(chunk) { country in
Cell(title: country.countryName ?? "Unknown")
}
if emptyElements > 0 {
ForEach(0..<emptyElements, id: \.self) { _ in
Cell(title: "Blank")
}
}
}
}
@ViewBuilder private func CountryListView(continentList: [Continent]) -> some View {
LazyVStack(spacing: 0) {
ForEach(continentList, id: \.self) { stack in
Grid(stack: stack, columns: 3)
}
}
}
var body: some View {
VStack {
ScrollView {
CountryListView(continentList: continents)
}
HStack {
Button {
self.continents = ContentView.continentListAsia
} label: {
Text("Asia")
}
Button {
self.continents = ContentView.continentListAfrica
} label: {
Text("Africa")
}
}
}
.padding()
}
private static let continentListAfrica: [Continent] = [
Continent(name: "Africa", countries: [
Country(regionCode: "dz", index: 1),
Country(regionCode: "bi", index: 2),
Country(regionCode: "cm", index: 3),
Country(regionCode: "td", index: 4),
Country(regionCode: "km", index: 5),
Country(regionCode: "eg", index: 6),
Country(regionCode: "er", index: 7),
Country(regionCode: "sz", index: 8),
Country(regionCode: "et", index: 9),
Country(regionCode: "gm", index: 10),
Country(regionCode: "gh", index: 11),
Country(regionCode: "gn", index: 12),
Country(regionCode: "ke", index: 13),
Country(regionCode: "lr", index: 14),
Country(regionCode: "ly", index: 15),
Country(regionCode: "mw", index: 16),
Country(regionCode: "mr", index: 17),
Country(regionCode: "mu", index: 18)
])
]
private static let continentListAsia: [Continent] = [
Continent(name: "Asia", countries: [
Country(regionCode: "af", index: 1),
Country(regionCode: "bd", index: 2),
Country(regionCode: "bt", index: 3),
Country(regionCode: "bn", index: 4),
Country(regionCode: "ps", index: 5),
Country(regionCode: "id", index: 6),
Country(regionCode: "ir", index: 7),
Country(regionCode: "iq", index: 8),
Country(regionCode: "kw", index: 9),
Country(regionCode: "lb", index: 10),
Country(regionCode: "my", index: 11),
Country(regionCode: "mv", index: 12),
Country(regionCode: "mm", index: 13),
Country(regionCode: "om", index: 14),
Country(regionCode: "pk", index: 15),
Country(regionCode: "qa", index: 16),
Country(regionCode: "sa", index: 17),
Country(regionCode: "sg", index: 18)
])
]
}
struct Cell: View {
private let title: String
public init(title: String) {
self.title = title
}
public var body: some View {
ZStack {
Text(title.prefix(10))
.background(Color.purple)
}.frame(width: 125, height: 125)
.background(Color.green)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
My team has been debugging problems with the SwiftUI List component this week.
We have found that it's performance is sub-optimal on iOS 16. You can see a simple grid of images, the scroll indicator stutters when scrolling down:
Now compare it to what happens when we use a ScrollView with a LazyVStack:
(An error occurred while uploading my second image, but pretend you see a scroll indicator moving smoothly down the side of the screen).
You can see the scroll indicator moves smoothly without issue.
We also found that the ScrollView combined with a LazyVStack properly calls onDisappear, which enables us to call a cancel method on the async image loading code that we use for our individual cells in this example. Though in a previous question, it was asserted that onDisappear cannot be reliably expected to be called in a List, I do not feel that answer is correct or proper behavior.
Is this a bug, or is this expected behavior on a List?
This is the cell that is being rendered:
struct UserGridCell: View {
let stackId: String
let user: ProfileGridCellUIModel
let userGridCellType: UserGridCellType
@State var labelFrame: CGRect = .zero
private var isOnlineAcessibilityValue: String {
return user.isOnline == true ? "" : ""
}
init(stackId: String,
user: ProfileGridCellUIModel,
userGridCellType: UserGridCellType
) {
self.stackId = stackId
self.user = user
self.userGridCellType = userGridCellType
}
var body: some View {
GeometryReader { containerGeometry in
ZStack(alignment: .bottom) {
HStack(spacing: 4) {
let statusAccentColor: Color = .red
Circle()
.frame(width: 8, height: 8)
.foregroundColor(statusAccentColor)
Text(String(user.remoteId) ?? "")
.lineLimit(1)
.foregroundColor(.black)
.overlay(GeometryReader { textGeometry in
Text("").onAppear {
self.labelFrame = textGeometry.frame(in: .global)
}
})
}
.frame(maxWidth: .infinity, alignment: .bottomLeading)
.padding(.leading, 8)
.padding(.trailing, 8)
.padding(.bottom, 8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.contentShape(Rectangle())
.accessibilityLabel(Text(user.name ?? ""))
.accessibilityValue(isOnlineAcessibilityValue)
}
.background(
ZStack {
AsyncProfileImage(request: URLRequest(url: URL(string: "https://picsum.photos/id/\(100 + user.remoteId)/200/300")!))
}
.accessibilityHidden(true)
)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(.red, lineWidth: user.hasAnyUnreadMessages ? 4 : 0)
)
.cornerRadius(4)
}
}
This is the code that renders each cell:
struct ProfileGrid: View {
public static var AspectRatio: CGFloat = 0.75
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.redactionReasons) private var reasons
private let stacks: [ProfileGridStackUIModel]
public init(stacks: [ProfileGridStackUIModel]
) {
self.stacks = stacks
}
var body: some View {
let columnCount: Int = 3
// If you use a list, you will get the stutter. If you use what you see below,
// it will render properly.
ScrollView {
LazyVStack {
ForEach(stacks, id: \.self) { stack in
Grid(stack: stack, columns: columnCount)
}
}
}
.buttonStyle(PlainButtonStyle())
.listStyle(PlainListStyle())
}
@ViewBuilder private func Grid(stack: ProfileGridStackUIModel, columns: Int) -> some View {
let chunks = stride(from: 0, to: stack.profiles.count, by: columns).map {
Array(stack.profiles[$0..<min($0 + columns, stack.profiles.count)])
}
ForEach(chunks, id: \.self) { chunk in
GridRow(chunk: chunk, stack: stack, columns: columns)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
@ViewBuilder private func GridRow(chunk: [ProfileGridCellUIModel], stack: ProfileGridStackUIModel, columns: Int) -> some View {
let emptyElements = columns - chunk.count
HStack(spacing: 8) {
ForEach(chunk) { user in
UserGridCell(stackId: "id",
user: user,
userGridCellType: .grid)
.aspectRatio(ProfileGrid.AspectRatio, contentMode: .fill)
}
if emptyElements > 0 {
ForEach(0..<emptyElements, id: \.self) { _ in
Rectangle()
.foregroundColor(Color.clear)
.contentShape(Rectangle())
.frame(maxWidth: .infinity)
.aspectRatio(ProfileGrid.AspectRatio, contentMode: .fill)
}
}
}
}
}
We have a bug reported where a user has a device with an en language and nil region code, and thus all NSLocalizedString lookups in are failing, meaning our string key is what is rendered onscreen. Thus, if we had this in our en.lproj/Localizable.strings file:
"some_key" = "Some string.";
It would render some_key instead of Some string. in our UI.
First question: how do I replicate this scenario locally? This question on Stack seems to almost describe the issue, but does not describe how one enters this state.
Second question: why would iOS not fall back to English in the event the region code was nil?
We have suddenly started seeing this crashing stack on iOS 16. Does anyone have any workarounds?
0
libsystem_platform.dylib
_platform_memmove + 420
arrow_right
1
vImage
vImageCopyBuffer + 824
2
libCGInterfaces.dylib
_vImageCreateCGImageFromBuffer + 2368
3
CoreSVG
CIImage* SVGFilterPrimitive::inputImage<CIImage>(SVGAtom::Name) const + 88
4
CoreSVG
SVGFilterPrimitive::drawFeComposite() const + 72
5
CoreSVG
SVGFilterPrimitive::selectPrimitive() const + 280
6
CoreSVG
SVGFilterPrimitive::draw(FilterContext&, FilterResult**, std::__1::unordered_map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, FilterResult*, std::__1::hash<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::equal_to<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::allocator<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const, FilterResult*> > >&, CGSize, CGSize) + 72
Can anyone explain why an iPhone might always be returning true for the UIScreen.isCaptured property, even after the device has been fully restarted and there is no screen mirroring or recording taking place? Are there any non-obvious scenarios where it would always be true? We have a user reporting an issue that can only be explained by this property incorrectly or erroneously coming back as true.
I have some customers reporting extreme keyboard lag when typing anything in our app. I have two movies demonstrating this behavior:
As well as this movie:
I really don't believe that this is related to any of our code -- this is the standard keyboard in both cases, and if somehow our code was blocking the UI thread the OS would kill our app.
We are unable to reproduce this behavior in-house and this only seems to apply to a handful of our customers.
If you search "keyboard lag" on Apple dev forums you will see numerous threads:
https://developer.apple.com/forums/thread/5071
https://developer.apple.com/forums/thread/79442?answerId=235108022#235108022
https://developer.apple.com/forums/thread/665191?answerId=656127022#656127022
https://developer.apple.com/forums/thread/671132
However, most of these threads are very old and it's not clear that there is or was a resolution.
This issue seems to have persisted across multiple releases of iOS 14 for our members. We opened a ticket with Apple DTS but they were unable to offer any guidance.
Has anyone here experienced this issue themselves, and can anyone offer a workaround beyond hoping that this is an iOS issue that Apple will fix in the future?
When using uploadTask(with:fromFile:) - https://developer.apple.com/documentation/foundation/urlsession/1411550-uploadtask to perform an upload in the background, should the fileUrl be kept in the temp directory:
NSTemporaryDirectory()
Or should it instead be the Caches directory?
NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)
I ask because I am starting to see this crashing stack:
Fatal Exception: NSInvalidArgumentException
Cannot read file at file:///private/var/mobile/Containers/Data/Application/XXXXXXXX-XXXX-XXXX-XXXX-CB39D5E080D9/tmp/org.alamofire.manager/multipart.form.data/XXXXXXXX-XXXX-XXXX-XXXX-28C54136FC0A
Coming from:
Request.swift - Line 583partial apply for closure #2 in UploadRequest.Uploadable.task(session:adapter:queue:) + 583
And I suspect that the file is maybe being deleted while also being actively used by a background uploadTask?
The Apple File System basics docs - https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW2 say this about data in the temporary directory:
[you] cannot rely on these files persisting after your app terminates
It says this about caches:
Cache data can be used for any data that needs to persist longer than temporary data, but not as long as a support file
NOTE: though this crashing stack is coming from Alamofire, - https://github.com/Alamofire/Alamofire I am in the process of re-writing it from scratch using pure-native Apple APIs, so assume for the purposes of this question that Alamofire is not part of the stack. Where should I be writing temporary files for upload?