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.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
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 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/xxx/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.
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 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.
Currently, we are implementing an undo/redo feature in UITextView.
However, we cannot use the built-in UndoManager in UITextView because we have multiple UITextView instances inside a UICollectionView.
Since UICollectionView recycles UITextView instances, the same UITextView might be reused in different rows, making the built-in UndoManager unreliable.
The shouldChangeTextIn method in UITextViewDelegate is key to implementing undo/redo functionality properly. Here is an example of our implementation:
extension ChecklistCell: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// Get the current text
let s = textView.text ?? ""
// Get the starting position of the change
let start = range.location
// Get the number of characters that will be replaced
let count = range.length
// Get the number of characters that will be added
let after = text.count
print(">>>> The current text = \"\(s)\"")
print(">>>> The starting position of the change = \(start)")
print(">>>> The number of characters that will be replaced = \(count)")
print(">>>> The number of characters that will be added = \(after)")
print(">>>>")
if let delegate = delegate, let checklistId = checklistId, let index = delegate.checklistIdToIndex(checklistId) {
delegate.attachTextAction(s: s, start: start, count: count, after: after, index: index)
}
return true
}
}
Working scene behind the UITextViewDelegate
However, this implementation does not work well with non-English input using an IME. When using an IME, there is an intermediate input before the final input is produced. For example, typing "wo" (intermediate input) produces "我" (final input). Currently, UITextViewDelegate captures both "wo" and "我".
UITextViewDelegate captures both "wo" and "我"
Is there a way to ignore the intermediate input from IME and only consider the final input?
In Android, we use the beforeTextChanged method in TextWatcher to seamlessly ignore the intermediate input from IME and only consider the final input. You can see this in action in this
Android captures only "我"
Is there an equivalent way in iOS to ignore the intermediate input from IME and only take the final input into consideration?
Currently, this is how I implement the drag and move operation:
collectionView.beginInteractiveMovementForItem
collectionView.updateInteractiveMovementTargetPosition
collectionView.endInteractiveMovement
The outcome looks like the following:
However, what I would like to achieve is the ability to customize the view of the "drop" location.
For instance, in the following example, a red line is drawn at the target drop location:
In this example, a transparent rectangle is drawn at the target drop location:
May I know how these apps achieve such an effect?
Thanks.
I am using NSCollectionLayoutSection.list as follow.
private func layoutConfig() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout { section, layoutEnvironment in
var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.headerMode = .supplementary
config.footerMode = .none
config.showsSeparators = false
config.headerTopPadding = 0
// https://developer.apple.com/forums/thread/759987
let layoutSection = NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
layoutSection.interGroupSpacing = 0
layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
if let header = layoutSection.boundarySupplementaryItems.first(where: { $0.elementKind == UICollectionView.elementKindSectionHeader }) {
header.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
}
return layoutSection
}
return layout
}
We provide our own custom header view and cell item view.
Header View
class HideShowHeader: UICollectionViewListCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize {
// Ensure the cell fills the width of the collection view
let size = CGSize(
width: targetSize.width,
height: 80
)
print(">>>> size \(size)")
return size
}
}
Cell Item View
class TodoCell: UICollectionViewListCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize {
// Ensure the cell fills the width of the collection view
let size = CGSize(
width: targetSize.width,
height: 80
)
return size
}
}
We would like to fine-tune the height of header and cell item.
However, override systemLayoutSizeFitting doesn't work.
May I know, when using NSCollectionLayoutSection.list, how to specific header height and cell item height?
Thanks.
Google has a very strict policy regarding "associated previously terminated accounts." This means that if you are associated with another previously banned developer, you will be banned too.
For instance : https://www.reddit.com/r/androiddev/comments/9mpyyi/google_play_developer_account_terminated_due_to/
Does Apple have a similar policy?
Recently, I was considering providing read-only access to an external party on App Store Connect. However, I am concerned that it might trigger the same risk.
Therefore, I am wondering if Apple has such a policy.
Thanks.
The following code, will create a red color text, without strike-through.
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let text = "Hello World"
let textCount = text.count
let fullRange = NSRange(location: 0, length: textCount)
var attributedText = NSMutableAttributedString(string: text)
attributedText.addAttribute(.foregroundColor, value: UIColor.green, range: fullRange)
attributedText.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: fullRange)
label.attributedText = attributedText
attributedText = NSMutableAttributedString(string: text)
attributedText.addAttribute(.foregroundColor, value: UIColor.red, range: fullRange)
attributedText.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: fullRange)
label.attributedText = attributedText
}
}
However, if I trigger label.text in between, it will cause the following strange behavior : A red color text, with strike-through created at the end of function.
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let text = "Hello World"
let textCount = text.count
let fullRange = NSRange(location: 0, length: textCount)
var attributedText = NSMutableAttributedString(string: text)
attributedText.addAttribute(.foregroundColor, value: UIColor.green, range: fullRange)
attributedText.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: fullRange)
label.attributedText = attributedText
// Why this will cause a red color text, with strike-through created at the end of function?
label.text = text
attributedText = NSMutableAttributedString(string: text)
attributedText.addAttribute(.foregroundColor, value: UIColor.red, range: fullRange)
attributedText.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: fullRange)
label.attributedText = attributedText
}
}
Does anyone what is the reason behind this behavior, and how I can avoid such? Thank you.
Topic:
UI Frameworks
SubTopic:
UIKit
Currently, I have achieve shadow and corner effect for UICollectionViewListCell, using the following code.
UICollectionViewListCell
class NoteCell: UICollectionViewListCell {
override func awakeFromNib() {
super.awakeFromNib()
initShadow()
initCorner()
}
private func updateShadowColor() {
// Determine the shadow color based on the current interface style
let shadowUIColor = UIColor.label
self.layer.shadowColor = shadowUIColor.cgColor
}
private func initShadow() {
// https://www.hackingwithswift.com/example-code/uikit/how-to-add-a-shadow-to-a-uiview
self.layer.shadowOpacity = 0.3
self.layer.shadowOffset = CGSize(width: 0.5, height: 0.5)
self.layer.shadowRadius = 2
self.layer.masksToBounds = false
self.updateShadowColor()
// Remove the following two lines if you experience any issues with shadow rendering:
self.layer.shouldRasterize = true
self.layer.rasterizationScale = UIScreen.main.scale
}
private func initCorner() {
var backgroundConfig = UIBackgroundConfiguration.listPlainCell()
backgroundConfig.backgroundColor = .systemBackground
backgroundConfig.cornerRadius = 16
self.backgroundConfiguration = backgroundConfig
}
layout
private func layoutConfig() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout { section, layoutEnvironment in
var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.headerMode = .none
config.footerMode = .none
config.showsSeparators = false
config.headerTopPadding = 0
config.backgroundColor = nil
config.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath in
guard let self = self else { return nil }
// Knowing what we are tapping at.
var snapshot = dataSource.snapshot()
let sectionIdentifier = snapshot.sectionIdentifiers[indexPath.section]
let itemIdentifiers = snapshot.itemIdentifiers(inSection: sectionIdentifier)
let itemIdentifier: NoteWrapper = itemIdentifiers[indexPath.item]
let deleteHandler: UIContextualAction.Handler = { action, view, completion in
completion(true)
// TODO:
//snapshot.reloadItems([itemIdentifier])
}
let deleteAction = UIContextualAction(style: .normal, title: "Trash", handler: deleteHandler)
var swipeActionsConfiguration = UISwipeActionsConfiguration(actions: [
deleteAction,
])
deleteAction.image = UIImage(systemName: "trash")
deleteAction.backgroundColor = UIColor.systemRed
swipeActionsConfiguration.performsFirstActionWithFullSwipe = false
return swipeActionsConfiguration
}
// https://developer.apple.com/forums/thread/759987
let layoutSection = NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
layoutSection.interGroupSpacing = 16 // Distance between item.
layoutSection.contentInsets = NSDirectionalEdgeInsets(
top: 16, // Distance between 1st item and its own header.
leading: 16,
bottom: 16, // Distance of last item and other header/ bottom edge.
trailing: 16
)
return layoutSection
}
return layout
}
This is the outcome.
However, when I perform swipe action, the shadow effect is gone.
Do you have any idea how I can resolve such? Thanks.
After waiting for few hours, I am still not able to edit the poster frame of app preview.
Can anyone from App Store Connect assist me on this?
Thank you.
Topic:
App Store Distribution & Marketing
SubTopic:
App Store Connect
Tags:
App Store
App Store Connect
This documentation describes what kind of data we should be sending to Apple server, once we are receiving CONSUMPTION_REQUEST
https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest
But, it doesn't describe what kind of data we are receiving, when we are receiving CONSUMPTION_REQUEST?
May I know, is such a document available?
Thank you.
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
App Store Server Notifications
App Store Server API
App Store Server Library
Hi,
I am using Apple Search Ads (Basic), and I’ve noticed that my bank account has been debited weekly.
However, when I recently tried to access the reports, they appear empty. Please see the attached screenshots for reference.
Is there a customer support representative from Apple Search Ads who can assist me with this issue?
Thank you.
Topic:
App Store Distribution & Marketing
SubTopic:
General
Tags:
App Store
Marketing
Apple Search Ads
We offer a 3-day free trial, and our paywall clearly states that users will be charged after the trial ends.
However, some users request refunds after the charge - even after fully using our app for days or even weeks. In some cases, refunds are approved despite the users having consumed our AI processing services for up to a month.
Since our app relies on backend AI processing, each user session incurs a real cost. To prevent losses, we utilize RevenueCat’s CONSUMPTION_REQUEST system and have set our refundPreference to: "2. You prefer that Apple declines the refund".
Until recently, Apple typically respected this preference, and 90% of refund requests were declined as intended.
However, starting about a week ago, we observed a sudden reversal: Apple is now approving around 90% of refund requests, despite our refund preference. As a result, we are operating at a loss and have had to halt both our marketing campaigns and our 3-day free trial.
We’re trying to understand whether this shift is due to a change in Apple’s refund policy, or if we need to handle CONSUMPTION_REQUEST differently on our end.
Has anyone else experienced similar changes? Any insights would be greatly appreciated.
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
Subscriptions
StoreKit
App Store Server Notifications
App Store Server Library