I notice that, normal implementation for UICollectionView + UISearchController, will not able to achieve smooth scrolling animation when hiding search bar.
As you can see in the below video, when we scroll upward the page, it seems like "the page has slipped up suddenly".
If we compare the animation with search bar in iOS Settings page, the problem is more obvious. iOS Settings page able to have a smooth scrolling experience.
Implementation
The following is our implementation. The complete workable project is found at : https://github.com/yccheok/demo-uicollectionview-uisearchcontroller
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
@IBOutlet weak var collectionView: UICollectionView!
let reuseIdentifier = "cell" // also enter this string as the cell identifier in the storyboard
var items = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48","1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48","1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "bye"]
private lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: UIViewController())
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = true
searchController.searchBar.placeholder = "search_todos"
searchController.searchBar.delegate = self
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
navigationItem.searchController = searchController
}
// MARK: - UICollectionViewDataSource protocol
// tell the collection view how many cells to make
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.items.count
}
// make a cell for each cell index path
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! MyCollectionViewCell
// Use the outlet in our custom class to get a reference to the UILabel in the cell
cell.label.text = self.items[indexPath.item]
cell.backgroundColor = .yellow
return cell
}
// MARK: - UICollectionViewDelegate protocol
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// handle tap events
print("You selected cell #\(indexPath.item)!")
}
}
class MyCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var label: UILabel!
}
extension ViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
}
}
extension ViewController: UISearchBarDelegate {
}
Do you have any idea, how we can achieve such a smooth scrolling animation? Thanks.
Post
Replies
Boosts
Views
Activity
I have developed an interactive widget which looks as the following
It is using CoreData.
The view is implemented as the following
struct widget_extensionEntryView : View {
var body: some View {
if let nsTodoList = nsTodoList {
VStack(alignment: .leading, spacing: 1) {
...
ForEach(0..<prefixGoodNSTodoArray.count, id: \.self) { index in
...
let todoIntent = TodoIntent(objectIDAsString: objectIDAsString, parentOrder: parentOrder)
Button(intent: todoIntent) {
}
}
}
}
}
}
The AppIntent is implemented as the following
import Foundation
import AppIntents
import WidgetKit
import CoreData
struct TodoIntent: AppIntent {
static var title: LocalizedStringResource = "Complete Task"
static var description: IntentDescription = IntentDescription("Complete selected task")
@Parameter(title: "objectIDAsString")
var objectIDAsString: String
@Parameter(title: "parentOrder")
var parentOrder: Int
init() { }
init(objectIDAsString: String, parentOrder: Int) {
self.objectIDAsString = objectIDAsString
self.parentOrder = parentOrder
}
func perform() async throws -> some IntentResult {
guard let objectID = NSManagedObjectID.from(objectIDAsString) else { return .result() }
guard let nsTodoList = NSTodoListRepository.INSTANCE.getNSTodoList(objectID) else { return .result() }
nsTodoList.toggleChecked(context: CoreDataStack.INSTANCE.viewContext, parentOrder: Int64(parentOrder))
RepositoryUtils.saveContextIfPossible(CoreDataStack.INSTANCE.viewContext)
TodoWidgetOptions.isWrittenByWidgetExtension = true
// Refresh all home widgets.
// TODO: https://developer.apple.com/forums/thread/721937
WidgetCenter.shared.reloadAllTimelines()
return .result()
}
}
The interactive widget works pretty well.
However, tapping on the widget has no response in the following situations:
After an overnight, we turn on the iPhone's screen and tap on the widget.
After a few hours of idle time, we turn on the iPhone's screen and tap on the widget.
One of the steps below will make the widget workable again:
Launch and close the main app again. The main app will call WidgetCenter.shared.reloadAllTimelines() during sceneDidEnterBackground.
Press and hold on the widget, choose 'Edit widget', and select the desired Todo list.
One thing to take note of is that I am using IntentTimelineProvider instead of AppIntentTimelineProvider. The reason I am using 'older tech' is due to the limitation mentioned in https://developer.apple.com/forums/thread/741053
However, I am not sure whether this is the root cause of the problem.
Does anyone have any idea why such a problem occurs?
Thanks.
Before iOS17, when we implement "Edit widget" feature with dynamic data, we are using the following ways.
Using an intent definition file
Using an intent extension
Here's the outcome.
A searchable view controller
Multi sectioned data view controller
Here is the implementation
intent definition file + intent extension
class IntentHandler: INExtension, ConfigurationIntentHandling {
func provideStickyNoteWidgetItemOptionsCollection(for intent: ConfigurationIntent, with completion: @escaping (INObjectCollection<StickyNoteWidgetItem>?, Error?) -> Void) {
var stickyNoteWidgetItems = [StickyNoteWidgetItem]()
var archivedStickyNoteWidgetItems = [StickyNoteWidgetItem]()
let allStickyNoteWidgetItems = NSPlainNoteRepository.getStickyNoteWidgetItemsWithoutTrash()
for allStickyNoteWidgetItem in allStickyNoteWidgetItems {
if allStickyNoteWidgetItem.archived == 1 {
archivedStickyNoteWidgetItems.append(allStickyNoteWidgetItem)
} else {
stickyNoteWidgetItems.append(allStickyNoteWidgetItem)
}
}
var sections = [INObjectSection<StickyNoteWidgetItem>]()
if !stickyNoteWidgetItems.isEmpty {
let section = INObjectSection(title: nil, items: stickyNoteWidgetItems)
sections.append(section)
}
if !archivedStickyNoteWidgetItems.isEmpty {
let archivedSection = INObjectSection(title: "archive".localized, items: archivedStickyNoteWidgetItems)
sections.append(archivedSection)
}
let collection = INObjectCollection(sections: sections)
completion(collection, nil)
}
override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
return self
}
}
However, if I were using AppIntent in iOS17, I can only achieve the following
Not searchable.
Not section-able.
Using AppIntent
import Foundation
import AppIntents
@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)
struct StickyNoteWidgetItemAppEntity: AppEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "StickyNoteWidgetItem")
@Property(title: "archived")
var archived: Bool?
struct StickyNoteWidgetItemAppEntityQuery: EntityQuery {
func entities(for identifiers: [StickyNoteWidgetItemAppEntity.ID]) async throws -> [StickyNoteWidgetItemAppEntity] {
// TODO: return StickyNoteWidgetItemAppEntity entities with the specified identifiers here.
return []
}
func suggestedEntities() async throws -> [StickyNoteWidgetItemAppEntity] {
// TODO: return likely StickyNoteWidgetItemAppEntity entities here.
// This method is optional; the default implementation returns an empty array.
return [
StickyNoteWidgetItemAppEntity(id: "id0", displayString: "note 0"),
StickyNoteWidgetItemAppEntity(id: "id1", displayString: "note 1"),
StickyNoteWidgetItemAppEntity(id: "id2", displayString: "note 2")
]
}
}
static var defaultQuery = StickyNoteWidgetItemAppEntityQuery()
var id: String // if your identifier is not a String, conform the entity to EntityIdentifierConvertible.
var displayString: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(displayString)")
}
init(id: String, displayString: String) {
self.id = id
self.displayString = displayString
}
}
By using AppIntent, I would like to achieve what is previously achievable using intent defination file + intent extension
Is that ever possible?
Thanks.
I'm using the UISheetPresentationController to present a view, as shown in the following code:
@IBAction func click(_ sender: Any) {
let whiteViewController = WhiteViewController.instanceFromMainStoryBoard()
if let sheet = whiteViewController.sheetPresentationController {
sheet.detents = [.medium()]
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
sheet.prefersGrabberVisible = false
// Remove dim effect.
////sheet.largestUndimmedDetentIdentifier = .medium
}
self.present(whiteViewController, animated: true)
}
The outcome is as follow
This code results in a presentation with:
A dimmed background.
A 'Touch anywhere to dismiss' behavior.
However, I want to remove the dim background while keeping the 'Touch anywhere to dismiss' functionality. When I add the line:
sheet.largestUndimmedDetentIdentifier = .medium
The dim background is indeed removed, but the touch-to-dismiss behavior is gone too, as illustrated:
Is there a way to achieve both – removing the dim background and retaining the touch-to-dismiss functionality?
Thank you.
I have the following HTML string. We want to render image from our app AppGroup.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<title>This is title</title>
</head>
<body>
<p><h1>This is title</h1></p>
<div style="font-size: 0">
<img src="file:///Users/***/Library/Developer/CoreSimulator/Devices/A7B89802-9C65-4512-85A7-51C4372172D0/data/Containers/Shared/AppGroup/14DA3695-BFAF-4096-9F54-2874FD8285C2/attachment/b16c714e-9bb5-4eaa-924e-e043a69088ea.jpeg" width="100%">
</div>
This is body text
</body>
</html>
However, if we execute the following code
let html = ...
// wkWebView is WKWebView
wkWebView.loadHTMLString(html, baseURL: nil)
Only text is rendered. The image is not rendered.
May I know, what kind of configuration is required, so that WKWebView able to render image files in AppGroup directory?
Thanks.
I was able to avoid NSInternalInconsistencyException by using
NSDiffableDatasource with NSFetchedResultsController
My data source definition is
private typealias DataSource = UICollectionViewDiffableDataSource<Int, NSManagedObjectID>
(Does anyone know why we should use Int as SectionIdentifierType? I find it works fine too, if I am using String as SectionIdentifierType)
Even though I do not see NSInternalInconsistencyException anymore, I have the following crash log from Firebase Crashlytics. Do you know how to fix that?
Fatal Exception: NSRangeException
0 CoreFoundation 0x99288 __exceptionPreprocess
1 libobjc.A.dylib 0x16744 objc_exception_throw
2 CoreFoundation 0x1a4318 -[__NSCFString characterAtIndex:].cold.1
3 CoreFoundation 0x928c8 -[NSArray subarrayWithRange:]
4 CoreData 0x702dc -[_PFArray subarrayWithRange:]
5 CoreData 0xc2d58 -[_NSDefaultSectionInfo objects]
6 CoreData 0x14eb98 -[NSFetchedResultsController _conditionallyDispatchSnapshotToDelegate:updatesInfo:]
7 CoreData 0xc3edc -[NSFetchedResultsController performFetch:]
My NSFetchedResultsController look as following
private lazy var fetchedResultsController: NSFetchedResultsController<NSPlainNote> = {
let fetchRequest: NSFetchRequest<NSPlainNote> = NSPlainNote.fetchRequest(
label: label,
propertiesToFetch: ["pinned"]
)
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: CoreDataStack.INSTANCE.viewContext,
sectionNameKeyPath: "pinned",
cacheName: nil
)
controller.delegate = fetchedResultsControllerDelegate
return controller
}()
My NSPlainNote.fetchResult looks as following
static func fetchRequest(label: String?, propertiesToFetch: [Any]?) -> NSFetchRequest<NSPlainNote> {
let fetchRequest = NSPlainNote.fetchRequest()
fetchRequest.propertiesToFetch = propertiesToFetch
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "pinned", ascending: false),
NSSortDescriptor(key: "order", ascending: true)
]
if let label = label {
let predicate = NSPredicate(format: "archived = false AND trashed = false AND label = %@", label)
fetchRequest.predicate = predicate
} else {
let predicate = NSPredicate(format: "archived = false AND trashed = false")
fetchRequest.predicate = predicate
}
return fetchRequest
}
I am not able to reproduce the problem.
However, we can observe crash happens during NSFetchedResultsController.performFetch.
For those who are experience in this, do you know what is the root cause, and how I can resolve such?
Thanks.
I install and test my own app, in real device, from TestFlight.
I want to able to keep testing on the in-app purchase/ subscription feature.
I notice once I have purchase an item success (I was prompted to enter password during purchase), it will forever be marked as purchased.
In simulator, if I uninstall then re-install again the app, the in-app purchase status will all be reset.
However, for TestFlight in real device, I do not find a way to reset my purchase. (So that I can keep testing on the purchase process)
Does anyone know, how I can reset/ cancel my in-app purchase status from TestFlight account?
I would like to implement in-note text search feature, as found in Apple's Notes, Apple's Safari app. It looks like the following
I understand that such an API is called UIFindInteraction, and only available in iOS16.
https://developer.apple.com/documentation/uikit/uifindinteraction
WWDC 2022: Adopt desktop-class editing interactions
However, my app is targeting iOS 15.
I was wondering, is it possible for us to provide same feature, in iOS 15?
Thank you.
We would like to know, whether a user system locale is using 12-hours or 24-hours format?
There are many proposed solutions at https://stackoverflow.com/q/1929958/72437
One of the solutions are
let formatString = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current)!
let hasAMPM = formatString.contains("a")
However, to me, this is not a correct solution.
When I tested with de_DE (German is using 24-hours), the returned string is HH 'Uhr'
What is Uhr mean for? I guess it mean "clock" in German?
There are so many other locales and one of them might contain letter a.
Does anyone know, what is a correct way, to check whether user system locale is using 12-hours or 24-hours format?
My app starts to introduce AdMob.
In App Store Connect page, when I perform editing under App Privacy/ Data Types
The input choice will be
applicable to both "Data Used to Track You" and "Data Not Linked to You"
Does anyone know how we can only edit "Data Not Linked to You" only?
Am I doing something not right?
Also, I notice that I am not allow to uncheck "Device ID" and then submit (For testing purpose).
Is it because I am using NSUserTrackingUsageDescription in my app?
The following is a code example from widget extension.
By using .environment(.colorScheme, ...), I am able to update view with correct theme aware named color.
However, I am not able to retrieve the correct RGB value, from the theme aware named color.
private func getColorScheme() -> ColorScheme {
if ... {
return ColorScheme.dark
} else {
return ColorScheme.light
}
}
@ViewBuilder
func contentView() -> some View {
// Light/ dark theme aware
let color = SwiftUI.Color("yellowNoteColor")
calculate(color)
HStack {
...
}
.background(color)
.environment(\.colorScheme, getColorScheme())
}
func calculate(_ color: SwiftUI.Color) {
var a: CGFloat = 0.0
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
let uiColor = UIColor(self)
uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
// !!! Always get the light theme color value !!!
}
Inside calculate function, the retrieved value is always in the light theme color value.
My guess is, caculate function is executed before .environment(\.colorScheme, getColorScheme()), that's why we are getting light theme color value always.
May I know, how to get correct RGB value from theme aware named color?
In main app, I can override dark/ light theme based on user preference, so that I can retrieve correct color information based on named color.
if (user preference) {
overrideUserInterfaceStyle = .light
} else {
overrideUserInterfaceStyle = .dark
}
// Dark theme/ light theme automatic aware color.
SwiftUI.Color("purpleColor")
However, how can I override the theme of a WidgetKit, so that my WidgetKit can interpret named color correctly?
I know in WidgetKit, I can read what is current system wide theme settings using
@Environment(\.colorScheme)
But, that is not what I want.
I want the ability to override theme of a WidgetKit based on user preference, then able to retrieve correct named color.
Thanks.
Our widget extension appearance, is dependent on the main app content.
The only way for us to keep the widget(s) up-to-date, is to perform the following during app inactive.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneDidEnterBackground(_ scene: UIScene) {
// Refresh home widgets.
WidgetCenter.shared.reloadAllTimelines()
This is always called regardless whether there is widget being placed.
I was wondering, is this consider a good practice?
I do not wish to perform unnecessary operation, which will drain up user battery.
We plan to develop a Share extension.
The CoreData is storing its SQLite file in AppGroup folder.
The share extension will run in a different process than main app's.
Since share extension and main app are running in different process, both will have their own instance of CoreData. However, even there are multiple instances of CoreData in different processes, their underlying are pointing to a single same SQLite file.
Under https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1
It mentions
When you set up a shared container, the containing app—and each contained app extension that you allow to participate in data sharing—have read and write access to the shared container. To avoid data corruption, you must synchronize data accesses. Use Core Data, SQLite, or Posix locks to help coordinate data access in a shared container.
But it isn't clear, what are detailed steps required to
How can we synchronize access to CoreData among 2 processes?
Who should responsible to consume relevant store change, and update the single token file? (https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes) Should it be main app, share extension or both?
Thank you.
Is there any static analysis tool which can help us detect memory leak cause by missing [weak self]?
I was wondering, is there any tool, which can help to detect memory leak caused by missing [weak self].
For instance, the following code contains memory leak issue caused by lack of [weak self]
class ImagePageViewController: UIPageViewController {
lazy var memoryLeak = UIAction(
title: "memory_leak",
image: nil
) { _ in
print(">>>> \(self)")
}
}
Is there any tool which can help us to prevent such issue? I have tried
https://github.com/realm/SwiftLint
but it is not able to detect such.