Calling mergeChanges on a managed object context will automatically refresh any managed objects that have changed. This ensures that your context always contains all the latest information. Note that you don’t have to call mergeChanges on a viewContext when you set its automaticallyMergesChangesFromParent property to true. In that case, Core Data will handle the merge on your behalf.
While you can fetch data from Core Data with @FetchRequest just fine, I tend to avoid it in my apps. The main reason for this is that I’ve always tried to separate my Core Data code from the rest of my application as much as possible.
This can be misleading. When one see parent store he can understand the parent persitent store of my context hierarchy. However, what is meant by parent store is either: the persistentStoreCoordinator or the parentContext So, if your context was setup with a parent context, changes are commited to his parent but no further. To save it to the persistent store, you’ll need to call save() on contexts all the way up in the hierarchy until you reach the persistent store.
When you save changes in a context, the changes are only committed “one store up.” If you save a child context, changes are pushed to its parent. Changes are not saved to the persistent store until the root context is saved. (A root managed object context is one whose parent context is nil.)
An object that meets the criteria specified by request (it is an instance of the entity specified by the request, and it matches the request’s predicate if there is one) and that has been inserted into a context but which is not yet saved to a persistent store, is retrieved if the fetch request is executed on that context.
Changes are not reflected in the context”. So what you’re seeing is normal. Batch updates work directly on the persistent store file instead of going through the managed object context, so the context doesn’t know about them. When you delete the objects by fetching and then deleting, you’re working through the context, so it knows about the changes you’re making (in fact it’s performing those changes for you).
mainContext would fetch all the way to the persistent store. Fetches and objectWithID: only go as many levels as they need to. It’s important to remember that when a context is created it’s a “snapshot” of the state of it’s parent. Subsequent changes to the parent will not be visible in the child unless the child is somehow invalidated
RootContext (private queue) - saves to persistent store MainContext (main queue) child of RootContext - use for UI (FRC) WorkerContext (private queue) - child of MainContext - use for updates & inserts
We need to use a custom Binding to trigger onChange as onEditingChanged is only called when the user selects the textField, and onCommit is only called when return or done button on keyboard is tapped.
Some apps want to support alternative app icons in Settings where user can choose to update app icon. Here’s some must do to make it work, as of Xcode 12.2
In Info.plist, must declare CFBundleIcons with both CFBundlePrimaryIcon and CFBundleAlternateIcons
Icons must be in project folder, not Asset Catalog
Prepare icons, like Icon1, Icon2 with 2 variants for 2x and 3x. These need to be added to project folder. When I add these to Asset Catalog it does not update app icon. I usually add a new folder in project like Resource/AlternateAppIcons
The default 1x size for iPhone icon size from iOS 7 to iOS 14 is 60px.
Although declaring CFBundlePrimaryIcon seems unnecessary, I find that without this it does not work. I use AppIcon60x60 as the compiled one from Asset Catalog for our main AppIcon
For UIPrerenderedIcon, a boolean value indicating whether the app’s icon already contains a shine effect. We set to false to allow iOS to apply round and glossy effect to our icon.
Since we’re using Asset Catalog, if we simply use UIImage with named, it will load by default in Asset Catalog, unless we manually specify bundle path. For simple demo, we can just copy our images again to Asset Catalog so we can show in the app
I usually have an enum of AlternateAppIcon so I can display in a grid and let user choose
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
enumAlternateAppIcon: String, CaseIterable, Identifiable{ var id: AlternateAppIcon { self }
case main = "Main" case pride1 = "Pride1" case pride2 = "Pride2" }
From iOS 13, the default is to support multiple scene, so the the old UIApplicationDelegate lifecycle does not work. Double check your Info.plist for UIApplicationSceneManifest key
Auto Layout has been around since macOS 10.7 and iOS 6.0 as a nicer way to do layouts over the old resizing masks. Besides some rare cases when we need to manually specify origins and sizes, Auto Layout is the preferred way to do declarative layouts whether we choose to do UI in code or Storyboard. The Auto Layout APIs have some improvements over the years, there are also some sugar libraries that add easy syntaxes, so more choices for developers.
In this article, let’s take a review of how layout has improved over the years, from manual layout, autoresizing masks and finally to Auto Layout. I will mention a few takes on some libraries and how to abstract Auto Layout using the builder pattern. There are also some notes about the view life cycle and APIs improvements that might be overlooked. These are of course based on my experience and varies because of personal tastes but I hope you‘ll find something useful.
Although the article mention iOS, the same learning applies to watchOS, tvOS and macOS too.
Positioning a view before Auto Layout
When I first started iOS programming in early 2014, I read a book about Auto Layout and that book detailed lots of scenarios that completely confused me. It didn’t take long until I tried Auto Layout in an app and I realized it w as so simple. In its simplest sense, a view needs a position and a size to be correctly shown on the screen, everything else is just extra. In Auto Layout’s term, we need to specify enough constraints to position and size the view.
Manual layout using CGRect
If we take a look back at the way we do a manual layout with the frame, there are origins and sizes. For example, here is how to position a red box that stretches accordingly with the width of the view controller.
viewDidLoad is called when the view property of view controller is loaded, we need to wait until viewDidLayoutSubviews so that we have access to the final bounds. When the bounds change for a view controller’s view, the view adjusts the positions of its subviews and then the system calls this method.
Why is view size correct in viewDidLoad
viewDidLoad is definitely not the recommended way to do manual layout but there are times we still see that its view is correctly sized and fills the screen. This is when we need to read View Management in UIViewController more thoroughly:
Each view controller manages a view hierarchy, the root view of which is stored in the *view*property of this class. The root view acts primarily as a container for the rest of the view hierarchy. The size and position of the root view is determined by the object that owns it, which is either a parent view controller or the app’s window. The view controller that is owned by the window is the app’s root view controller and its view is sized to fill the window. A view controller’s root view is always sized to fit its assigned space.
It can also be that the view has fixed size in xib or storyboard but we should control the size explicitly and do that in the right view controller method to avoid unexpected behaviors.
Autoresizing masks
Autoresizing mask was the old way to make layout a bit more declarative, also called springs and struts layout system. It is integer bit mask that determines how the receiver resizes itself when its superview’s bounds change. Combining these constants lets you specify which dimensions of the view should grow or shrink relative to the superview. The default value of this property is none, which indicates that the view should not be resized at all.
While we can specify which edges we want to fix and which should be flexible, it is confusing in the ways we do in xib and in code.
In the above screenshot, we pin the top of the red box to the top of the screen, and that is fixed distance. When the view changes size, the width and height of the red box changes proportionally, but the top spacing remains fixed.
In code, instead of specifying autoresizing in terms of fixed distance, we use flexible terms to specify which edges should be flexible.
To achieve the same red box on the top of the screen, we need to specify a flexible width and a flexible bottom margin. This means the left, right and top edges are fixed.
Horizontally fixed distance from the left: [.flexibleRightMargin]
Center horizontally [.flexibleLeftMargin, .flexibleRightMargin]
Vertically fixed distance from the top: [.flexibleBottomMargin]
Center vertically [.flexibleTopMargin, .flexibleBottomMargin]
These are not very intuitive and the way these are scaled proportionally may not fit our expectation. Also, note that multiple options can be done on the same axis.
When more than one option along the same axis is set, the default behavior is to distribute the size difference proportionally among the flexible portions. The larger the flexible portion, relative to the other flexible portions, the more it is likely to grow. For example, suppose this property includes the flexibleWidth and flexibleRightMarginconstants but does not include the flexibleLeftMargin constant, thus indicating that the width of the view’s left margin is fixed but that the view’s width and right margin may change.
Understanding of autoresizing masks won’t waste your time, we will come back to it in a few minutes 😉
Auto Layout to the rescue
Auto Layout, together with dynamic text and size classes, are recommended ways to build adaptive user interfaces as there the number of iOS devices with different screen sizes grows.
A constraint-based layout system
Auto Layout is described via NSLayoutConstraint by defining constraints between 2 objects. Here is the simple formula to remember:
Here is how to replicate that red box with NSLayoutConstraint. We need to specify which property of which view should be connected to another property of another view. Auto Layout supports many attributes such ascenterX, centerY and topMargin.
If we run the above code, we will get into the popular warning message regarding translatesAutoresizingMaskIntoConstraints
[LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0x600003ef2300 h=--& v=--& UIView:0x7fb66c5059f0.midX == 0 (active)>",
"<NSLayoutConstraint:0x600003e94b90 H:|-(20)-[UIView:0x7fb66c5059f0](LTR) (active, names: '|':UIView:0x7fb66c50bce0 )>"
)
If you need some help deciphering this, there is wtfautolayout that does a good job on explaining what’s really happening.
It is said that resizing masks has been reimplemented using Auto Layout under the hood, and there is always NSAutoresizingMaskLayoutConstraint added to the view, hence the midX constraint.
We should never mix resizing masks and Auto Layout to avoid unwanted behavior, the fix is simply to disable translatesAutoresizingMaskIntoConstraints
This property is false by default for views from xib or storyboard but holds true if we declare layout in code. The intention is for the system to create a set of constraints that duplicate the behavior specified by the view’s autoresizing mask. This also lets you modify the view’s size and location using the view’s frame, bounds, or center properties, allowing you to create a static, frame-based layout within Auto Layout.
Visual Format Language
The Visual Format Language lets you use ASCII-art like strings to define your constraints. I see it is used in some code bases so it’s good to know it.
Here is how to recreate the red box using VFL. We need to specify constraints for both horizontal and vertical direction. Note that the same format string may result in multiple constraints:
1 2 3 4 5 6
let views = ["box" : box] let horizontal = "H:|-20-[box]-20-|" let vertical = "V:|-50-[box(100)]" let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: horioptions: [], metrics: nil, views: views) let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: veoptions: [], metrics: nil, views: views) NSLayoutConstraint.activate([horizontalConstraints, verticalConstraints].flatMap({ $0 }))
Visual Format Language is a bit nicer than the verbose NSLayoutConstraint initializer, but it encourages string format, which is error-prone.
addConstraint and activate
This may seem trivial but I see in modern code bases, there is still usage of addConstraint . This was old and hard to use, as we must find the nearest common ancestor view of the 2 views that envolve in Auto Layout.
Starting from iOS 8, there is isActive and the static activate functions that ease this adding constraints process a lot. Basically what it does is to activate or deactivate the constraint with calls toaddConstraint(_:) and removeConstraint(_:) on the view that is the closest common ancestor of the items managed by this constraint.
NSLayoutAnchor
Starting with macOS 10.11 and iOS 9, there was NSLayoutAnchor that simplifies Auto Layout a lot. Auto Layout was declarative, but a bit verbose, now it is simpler than ever with an anchoring system.
The cool thing about NSLayoutAnchor is its generic constrained API design. Constraints are divided into X-axis, Y-axis and dimension anchor type that makes it hard to make mistakes.
open class NSLayoutXAxisAnchor : NSLayoutAnchor<NSLayoutXAxisAnchor>
open class NSLayoutDimension : NSLayoutAnchor<NSLayoutDimension>
For example, we can’t pin the top anchor to the left anchor as they are in different axis, and it makes no sense. Attempt to do the following results in compilation issue as Swift strong type system ensures correctness.
I’ve gone into a few cases where the error messages from NSLayoutAnchor don’t help. If we mistakenly connect topAnchor with centerXAnchor, which are not possible as they are from different axes.
Xcode is complaining about ‘Int’ is not convertible to ‘CGFloat’ which is very misleading. Can you spot the error?
The problem is that we are using equalToConstant , not equalTo . The generic constraints of NSLayoutAnchor is giving us misleading errors and can waste lots of time of us trying to figure out the subtle typos.
Abstractions over Auto Layout
NSLayoutAnchor is getting more popular now but it is not without any flaws. Depending on personal taste, there might be some other forms of abstractions over Auto Layout, namely Cartography and SnapKit, which I ‘ve used and loved. Here are a few of my takes on those.
Cartography
Cartography is one of the most popular ways to do Auto Layout in iOS. It uses operators which makes constraints very clear:
What I don’t like about Cartography is that we have to repeat parameter names and that the parameters inside closure are just proxies, not real views and there is a limit on the number of constrain items.
Another huge con was that long compilation time issue due to excessive use of operators Very long compile time in large functions. Although Swift compilation time is getting better, this was a big problem. I even had to write a script to remove Cartography to use simple NSLayoutAnchor, so take a look at AutoLayoutConverter, which converts Cartography code from
There are always tradeoffs but to reduce compilation time at the time was a top priority.
SnapKit
SnapKit, originally Masonry, is perhaps the most popular Auto Layout wrapper
box.snp.makeConstraints { (make) -> Void in
make.width.height.equalTo(50)
make.center.equalTo(self.view)
}
The syntax is nice and with snp namespace to avoid extension name clashes, which I love.
The thing I don’t like about SnapKit is that limited closure. We can only work on 1 view at a time, and the make inside closure is just a proxy, which does not seem intuitive.
Imagine if we’re gonna make paging views or piano, where each view is stacked side by side. We need a lot of SnapKit calls as we can only work on 1 view at a time. Also, there is no clear relationship where we connect with the other view.
We might exclude more than one properties or set different priority levels for each constraint. The simple wrapper with overloading functions and default parameters are just like building rigid abstraction based on premature assumptions. This just limits us in the long run and not scalable 😢
Embracing Auto Layout
All the Auto Layout frameworks out there are just convenient ways to build NSLayoutConstraint, in fact, these are what you normally need
Call addSubview so that view is in the hierarchy
Set translatesAutoresizingMaskIntoConstraints = false
Set isActive = true to enable constraints
Here is how to make an extension on NSLayoutConstraint that disables translatesAutoresizingMaskIntoConstraints for the involved views. Code is from Omnia
Here before we activate constraints, we find the firstItem then disables translatesAutoresizingMaskIntoConstraints. From Swift 4.2 there is a separation between compactMap and flatMap so we can safely use flatMap to flatten an array of arrays. This is useful when we have an array of arrays of constraints.
This is a very thin but powerful wrapper over NSLayoutAnchor and we can expand it the way we need. It sadly has some problems, like we can’t easily change the priority, as we have to reference the constraint 😢
Making Auto Layout more convenient with the builder pattern
The above extension on NSLayoutConstraint works well. However, if you’re like me who wants even more declarative and fast Auto Layout code, we can use the builder pattern to make Auto Layout even nicer. The builder pattern can be applied to many parts of the code but I find it very well suited for Auto Layout. The final code is Anchors on GitHub, and I will detail how to make it.
Here is what we want to achieve to quickly position 4 views:
Most of the times, we want to anchor to parent view, so that should be implicitly done for us. I like to have anchor namespace to avoid extension naming clashes and to make it the starting point for all our convenient Auto Layout code. Let’s identify a few core concepts
Which objects can interact with Auto Layout?
Currently, there are 3 types of objects that can interact withAuto Layout
UIView
UILayoutSupport, from iOS 7, for UIViewController to get bottomLayoutGuide and topLayoutGuide . In iOS 11, we should use safeAreaLayoutGuide from UIView instead
UILayoutGuide: using invisible UIView to do Auto Layout is expensive, that’s why Apple introduced layout guides in iOS 9 to help.
So to support these 3 with anchor namespace, we can make Anchor object that holds AnyObject as behind the scene, NSLayoutConstraint works with AnyObject:
public class Anchor: ConstraintProducer {
let item: AnyObject
/// Init with View
convenience init(view: View) {
self.init(item: view)
}
/// Init with Layout Guide
convenience init(layoutGuide: LayoutGuide) {
self.init(item: layoutGuide)
}
// Init with Item
public init(item: AnyObject) {
self.item = item
}
}
Now we can define anchor property
public extension View {
var anchor: Anchor {
return Anchor(view: self)
}
}
public extension LayoutGuide {
var anchor: Anchor {
return Anchor(layoutGuide: self)
}
}
Which properties are needed in a layout constraint?
The builder patterns make building things declaratively by holding temporary values. Besides from, to, priority, identifier, we need an array of pins to handle cases where there are multiple created constraints. A center constraint results in both centerX and centerY constraints, and an edge constraint results in top, left, bottom and right constraints.
enumTo{ case anchor(Anchor) case size casenone } classPin{ let attribute: Attribute var constant: CGFloa init(_ attribute: Attribute, constant: CGFloat = 0) { self.attribute = attribute self.constant = constant } let item: AnyObject // key: attribute // value: constant var pins: [Pin] = [ var multiplierValue: CGFloat = 1 var priorityValue: Float? var identifierValue: String? var referenceBlock: (([NSLayoutConstraint]) -> Void)? var relationValue: Relation = .equal var toValue: To = .none
With this, we can also expand our convenient anchor to support more constraints, like spacing horizontally, which adds left and right constraints with correct constants. Because as you know, in Auto Layout, for right and bottom direction, we need to use negative values:
There are times we want to infer constraints, like if we want a view’s height to double its width. Since we already have width, declaring ratio should pair the height to the width.
I see we’re used to storing constraint property in order to change its constant later. The constraints property in UIView has enough info and it is the source of truth, so retrieving constraint from that is more preferable.
Here‘s how we find constraint and update that
boxA.anchor.find(.height)?.constant = 100
// later
boxB.anchor.find(.height)?.constant = 100
// later
boxC.anchor.find(.height)?.constant = 100
The code to find constraint is very straightforward.
One of the patterns I see all over is resetting constraints in UITableViewCell or UICollectionViewCell. Depending on the state, the cell removes certain constraints and add new constraints. Cartography does this well by using group.
If we think about it, NSLayoutConstraint is just layout instructions. It can be activated or deactivated . So if we can group constraints, we can activate or deactivate them as a whole.
Here is how to declare 4 groups of constraints, the syntax is from Anchors but this applies to NSLayoutAnchor as well since those generateNSLayoutConstraint under the hood.
let g1 = group(box.anchor.top.left)
let g2 = group(box.anchor.top.right)
let g3 = group(box.anchor.bottom.right)
let g4 = group(box.anchor.bottom.left)
Where to go from here
In this article, we step a bit in time into manual layout, autoresizing masks and then to the modern Auto Layout. The Auto Layout APIs have improvements over the years and are recommended way to do layout. Learning declarative layout also helps me a lot when I learn Constraint Layout in Android, flexbox in React Native or the widget layout in Flutter.
The post goes through the detailed implementation of how we can build more convenient Auto Layout like Anchors with the builder pattern. In the next article, let’s explore the many ways to debug Auto Layout and how to correctly do Auto Layout for different screen sizes.
In the meantime, let’s play Tetris in Auto Layout, because why not 😉
From iOS 7, every app downloaded from the store has a receipt (for downloading/buying the app) at appStoreReceiptURL. When users purchases something via In App Purchase, the content at appStoreReceiptURL is updated with purchases information. Most of the cases, you just need to refresh the receipt (at appStoreReceiptURL) so that you know which transactions users have made.
Note
Receipt is generated and bundled with your app when user download the app, whether it is free or paid
When user makes IAP, receipt is updated with IAP information
When user downloads an app (download free, or purchase paid app), they get future updates (whether free or paid) forever.
Call SKReceiptRefreshRequest or SKPaymentQueue.restoreCompletedTransactions asks for Appstore credential
When we build the app from Xcode or download from Testflight, receipt is not bundled within the app since the app is not downloaded from AppStore. We can use SKReceiptRefreshRequest to download receipt from sandbox Appstore
restoreCompletedTransactions updates app receipt
Receipt is stored locally on device, so when user uninstalls and reinstalls your app, there’s no in app purchases information, this is when you should refresh receipt or restoreCompletedTransactions
Users restore transactions to maintain access to content they’ve already purchased. For example, when they upgrade to a new phone, they don’t lose all of the items they purchased on the old phone. Include some mechanism in your app to let the user restore their purchases, such as a Restore Purchases button. Restoring purchases prompts for the user’s App Store credentials, which interrupts the flow of your app: because of this, don’t automatically restore purchases, especially not every time your app is launched.
In most cases, all your app needs to do is refresh its receipt and deliver the products in its receipt. The refreshed receipt contains a record of the user’s purchases in this app, on this device or any other device. However, some apps need to take an alternate approach for one of the following reasons:
If you use Apple-hosted content, restoring completed transactions gives your app the transaction objects it uses to download the content. If you need to support versions of iOS earlier than iOS 7, where the app receipt isn’t available, restore completed transactions instead.
Refreshing the receipt asks the App Store for the latest copy of the receipt. Refreshing a receipt does not create any new transactions.
Restoring completed transactions creates a new transaction for every completed transaction the user made, essentially replaying history for your transaction queue observer.
Users sometimes need to restore purchased content, such as when they upgrade to a new phone.
Don’t automatically restore purchases, especially when your app is launched. Restoring purchases prompts for the user’s App Store credentials, which interrupts the flow of your app
In most cases, you only need to refresh the app receipt and deliver the products listed on the receipt. The refreshed receipt contains a record of the user’s purchases in this app, from any device the user’s App Store account is logged into
Refreshing a receipt doesn’t create new transactions; it requests the latest copy of the receipt from the App Store
Restoring completed transactions creates a new transaction for every transaction previously completed, essentially replaying history for your transaction queue observer. Your app maintains its own state to keep track of why it’s restoring completed transactions and how to handle them.
What are the different IAP types
From AppStore
Consumable (pay everytime)
A consumable In-App Purchase must be purchased every time the user downloads it. One-time services, such as fish food in a fishing app, are usually implemented as consumables.
Non-Consumable (one time payment)
Non-consumable In-App Purchases only need to be purchased once by users. Services that do not expire or decrease with use are usually implemented as non-consumables, such as new race tracks for a game app.
Auto-Renewable Subscriptions (will deduct money from your credit card on a cycle complete)
Auto-renewable Subscriptions allow the user to purchase updating and dynamic content for a set duration of time. Subscriptions renew automatically unless the user opts out, such as magazine subscriptions.
Free Subscription (no payment and is still visible even you did not submitted your account detail to itunes connect)
Free subscriptions are a way for developers to put free subscription content in Newsstand. Once a user signs up for a free subscription, it will be available on all devices associated with the user’s Apple ID. Note that free subscriptions do not expire and can only be offered in Newsstand-enabled apps.
Non-Renewing (need to renew manually)
Subscription Non-Renewing Subscriptions allow the sale of services with a limited duration. Non-Renewing Subscriptions must be used for In-App Purchases that offer time-based access to static content. Examples include a one week subscription to voice guidance feature within a navigation app or an annual subscription to online catalog of archived video or audio.
The receipt consists of a single file in the app bundle. The file is in a format called PKCS #7. The payload consists of a set of receipt attributes in a cross-platform format called ASN.1
1 2 3 4 5 6 7 8 9 10
case12: // Receipt Creation Date var dateStartPtr = ptr receiptCreationDate = readASN1Date(ptr: &dateStartPtr, maxLength: length)
case17: // IAP Receipt print("IAP Receipt.") case19: // Original App Version var stringStartPtr = ptr originalAppVersion = readASN1String(ptr: &stringStartPtr, maxLength: length)
Original Application Version The version of the app that was originally purchased. ASN.1 Field Type 19 ASN.1 Field Value UTF8STRING
Note
This corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in the Info.plist file when the purchase was originally made
CFBundleVersion is build number, and CFBundleShortVersionString is app version
1 2 3
In-App Purchase Receipt The receipt for an in-app purchase. ASN.1 Field Type 17
Verify your receipt first with the production URL; then verify with the sandbox URL if you receive a 21007 status code. This approach ensures you do not have to switch between URLs while your application is tested, reviewed by App Review, or live in the App Store.
Show me the code
Let’s use enum to represent possible states for each resource. Here’s simple case where we only have 1 non consumable IAP product.
enumIAPError: Error{ case failedToRefreshReceipt case failedToRequestProduct case failedToPurchase case receiptNotFound }
enumIAPResourceState<T> { case notAsked case loading case success(T) case failure(IAPError) }
finalclassPricingPlan: ObservableObject{ staticlet pro = (Bundle.main.bundleIdentifier ?? "") + ".pro"
@Published var isPro: Bool = false @Published var product: IAPResourceState<SKProduct> = .notAsked @Published var purchase: IAPResourceState<SKPayment> = .notAsked @Published var receipt: IAPResourceState<InAppReceipt> = .notAsked }
Let’s have a central place for managing all IAP operations, called IAPManager, it can update our ObservableObjectPricingPlan hence triggers update to SwiftUI.
funcrequestProducts() { let identifiers = PricingPlan.pro let request = SKProductsRequest(productIdentifiers: Set(arrayLiteral: identifiers)) request.delegate = self pricingPlan.product = .loading request.start() }
funcpurchase(product: SKProduct) { guardSKPaymentQueue.canMakePayments() else { showAlert(text: "You are not allowed to make payment. Please check device settings.") return }
pricingPlan.purchase = .loading let payment = SKPayment(product: product) paymentQueue.add(payment) }
You can use restoreCompletedTransactions if you simply finishTransaction and grant user pro feature, like in this simple tutorial In-App Purchase Tutorial: Getting Started, search for SKPaymentTransactionObserver. restoreCompletedTransactions also updates receipt.
Otherwise refreshing receipt is a better idea. It serves both case when receipt is not there locally and when you want to restore transactions. With receipt refreshing, no restored transactions are created and SKPaymentTransactionObserver is not called, so we need to check receipt proactively.
Either restoreCompletedTransactions or SKReceiptRefreshRequest asks for AppStore credential so you should present a button there and ask user.
Check local receipt
Try to locate local receipt and examine it.
If it is not there (missing, corrupted), refresh receipt
If it’s there, check if it was from a version when the app was still as paid. Notice the difference in meaning of originalAppVersion in macOS and iOS
If it is not paid, check if this receipt contains In App Purchase information for our product
In practice, we need to perform some basic checks on receipt, like bundle id, app version, device id. Read In-App Purchases: Receipt Validation Tutorial, search for Validating the Receipt. TPInAppReceipt also has some handy verify functions
Besides verifying receipt locally, it is advisable to call verifyreceipt either on device, or better on serve to let Apple verify receipt and returns you a human readable json for receipt information.
Supposed we want to present a ViewController, and there exist both UIToolbar in both the presenting and presented view controllers.
From iOS 13, the model style is not full screen and interactive. From UITests perspective there are 2 UIToolbar, we need to specify the correct one to avoid multiple match errors
1
let editButton = app.toolbars["EditArticle.Toolbar"].buttons["Edit"]
After has recently reminded about his updating APNs provider API which makes me realised a lot has changed about push notifications, both in terms of client and provider approach.
The HTTP/2-based Apple Push Notification service (APNs) provider API lets you take advantage of great features, such as authentication with a JSON Web Token, improved error messaging, and per-notification feedback. If you send push notifications with the legacy binary protocol, we strongly recommend upgrading to the APNs provider API.
From the the iOS client point of view, you pretty much don’t need to care about provider API, as you only need to register for push notification capability and send the device token to your backend, and it’s the backend’s job to send push request to Apple notifications server.
But it’s good to know the underlying mechanism, especially when you want to troubleshoot push notification. Since a push notification failure can be because of many reasons: it could be due to some failed logic in your backend, or that the push certificate has expired, or that you have sent the wrong device token, or that user has turned off push permission in Settings app, or that the push has been delayed.
Here’s also a common abbreviations you need to learn: APNs it is short for Apple Push Notification service. You can think of it as the server your device always has connection with, so you’re ready for any server push messages from it.
What is binary provider API
Push notification feature was first introduced by Apple in 2009 via a fairly complex binary provider API
In short, binary provider API is just a specification about which address and which format to send push request to Apple push server. The binary interface of the production environment is available through gateway.push.apple.com, port 2195 and development environment gateway.sandbox.push.apple.com, port 2195. The binary interface employs a plain TCP socket for binary content that is streaming in nature.
As you can see in the package instruction above, we need specify frame length and data. Data is a key value pairs of multiple informations like device token, expiration, topic, priority.
Send request to the above addresses via secured TLS or SSL channel. The SSL certificate required for these connections is obtained from your developer account.
The new HTTP/2 provider API
The new HTTP2/ provider API is recommended, it has detailed error reporting and better throughput. If you use URLSession which supports the HTTP/1.1 and HTTP/2 protocols. HTTP/2 support, as described by RFC 7540, you will feel familiar.
In short, when you have a notification to send to a user, your provider must construct a POST request and send it to Apple Push Notification service (APNs). Your request must include the following information:
The JSON payload that you want to send
The device token for the user’s device
Request-header fields specifying how to deliver the notification
For token-based authentication, your provider server’s current authentication token
Upon receiving your server’s POST request, APNs validates the request using either the provided authentication token or your server’s certificate. If validation succeeds, APNs uses the provided device token to identify the user’s device. It then tries to send your JSON payload to that device.
So it’s pretty much how you configure URLRequestwith URLSession, specify base url, some request headers including the authorization and the payload body.
Use HTTP/2 and TLS 1.2 or later to establish a connection to the new provider API endpoint. For development serverit is api.sandbox.push.apple.com:443 and for production server it is api.push.apple.com:443. You then send the request as POST and Apple will do its job to verify and send the push notification to the users device.
Certificate vs token based authentication
To send push request to APNs, you need to authenticate to tell that is really from you. APNs support 2 types of authentication, the traditional way with a certificate, and the recently new recommended way with a p8 token.
Certificate based authentication
For certificate based authentication, you will need a p12 certificate which you can obtain and generate from your Developer account.
Because there are sandbox and production push endpoint, few years ago it was required to have separate sandbox and production environment push certificate, which you had to create separately in your Apple developer account, then in testing need to specify the correct push certificate for each environment.
Now around iOS 10 year we can use just 1 single certificate for both sandbox and production, which is a relief for us developers. When we create certificate on Apple developer page, we need to upload a certificate signing request that we can generate from Keychain Access app. After we download the generated push certificate, download and run it in Keychain, there we can generate p12 key file that contains both certificate and private key to sign our push request.
Certificate and provisioning profiles valid for only 1 year. So every year you have to renew push notification certificates, which is also a good and bad thing. Besides that, every app differs from bundle id, so we kinda have to generate certificate for each app.
Token based authentication
Token-based authentication offers a stateless way to communicate with APNs. Stateless communication is faster than certificate-based communication because it doesn’t require APNs to look up the certificate, or other information, related to your provider server. There are other advantages to using token-based authentication:
The cool thing about token is that you can use one token to distribute notifications for all of your company’s apps. You can create in your Apple developer account. Key authentication is the recommended way to authenticate with Apple services, so it is used not only for push services, but also for MusicKit and DeviceCheck.
When you request a key, Apple gives you a 10-character string with the Key ID which you must include this string in your JSON tokens and an authentication token signing key, specified as a text file (with a .p8 file extension). The key is allowed to download once and you need to keep it properly.
The token that you include with your notification requests uses the JSON Web Token (JWT) specification. The token itself contains four key-value pairs
After all, the JSON Web Token is encoded in this authorization HTTP headers in your request like this
For security, APNs requires you to refresh your token regularly. Refresh your token no more than once every 20 minutes and no less than once every 60 minutes.
How to register for push notifications from iOS app
The APIs to register for remote notification has changed over the years.
iOS 7
In iOS 7, we used to use this method registerForRemoteNotificationTypes to register to receive remote notifications of the specified types via Apple Push Notification service.
The types can be UIRemoteNotificationTypeBadge, UIRemoteNotificationTypeAlert, UIRemoteNotificationTypeSound
When you send this message, the device initiates the registration process with Apple Push Notification service. If it succeeds, the app delegate receives a device token in the application:didRegisterForRemoteNotificationsWithDeviceToken: method; if registration doesn’t succeed, the delegate is informed via the application:didFailToRegisterForRemoteNotificationsWithError:method. If the app delegate receives a device token, it should connect with its provider and pass it the token.
iOS 8 with registerUserNotificationSettings
From iOS 8, there’s separation between asking for a remote notification with device token, and with presenting push message to the user. This confused developers as these 2 things are separate now.
First, we use registerForRemoteNotifications to register to receive remote notifications via Apple Push Notification service.
Call this method to initiate the registration process with Apple Push Notification service. If registration succeeds, the app calls your app delegate object’s application(_:didRegisterForRemoteNotificationsWithDeviceToken:) method and passes it a device token. You should pass this token along to the server you use to generate remote notifications for the device. If registration fails, the app calls its app delegate’s application(_:didFailToRegisterForRemoteNotificationsWithError:) method instead.
In short, this is to receive device token from APNs so we can do silent push notification or other things. Note that we need to enable Remote notification capability for Background modes.
To present push message to user via alert, banner, badge or sound, we need to explicitly ask for using this method registerUserNotificationSettings to registers your preferred options for notifying the user.
If your app displays alerts, play sounds, or badges its icon, you must call this method during your launch cycle to request permission to alert the user in these ways. After calling this method, the app calls the application(_ application: UIApplication, didRegister notificationSettings: UIUserNotificationSettings) method of its app delegate to report the results. You can use that method to determine if your request was granted or denied by the user.
iOS 10 with UserNotifications framework
In iOS 10, Apple introduced UserNotifications and UserNotificationsUI framework and lots of new features to push notifications like actions and attachments.
To ask for permission to present push message from iOS 10, use the new UNUserNotificationCenter which accepts options and block callback with grant status.
There ‘s also UNNotificationAction and UNNotificationAttachment to specify additional actions and attachment to go along with the push message, this is very handy for visual purpose and convenient actions user can perform right away from the push message.
There’s also a convenient UserNotificationsUI that was shipped with iOS 10 that allows us to embed custom view controller from our push message
When an iOS device receives a notification containing an alert, the system displays the contents of the alert in two stages. Initially, it displays an abbreviated banner with the title, subtitle, and two to four lines of body text from the notification. If the user presses the abbreviated banner, iOS displays the full notification interface, including any notification-related actions. The system provides the interface for the abbreviated banner, but you can customize the full interface using a notification content app extension.
Also, there is this callback userNotificationCenter _:willPresent that asks the delegate how to handle a notification that arrived while the app was running in the foreground.
If your app is in the foreground when a notification arrives, the shared user notification center calls this method to deliver the notification directly to your app. If you implement this method, you can take whatever actions are necessary to process the notification or show it when your app is running.
iOS 12 with provisional push
New in iOS 12 is the UNAuthorizationStatus.provisional, which are notifications that appear silently in the user’s notification center without appearing on the user’s home screen. We can start sending them as soon as a user has downloaded and run your app for the first time. You can send provisional push notifications unlimited times unless the user explicitly turns them off.
This is good to send unobtrusive push to users in their Notification Center where they can pick up at a later time.
iOS 13 with apns-push-type
Starting with iOS 13 and watchOS 6, there is apns-push-type which must accurately reflect the contents of your notification’s payload. If there is a mismatch, or if the header is missing on required systems, APNs may return an error.
The apns-push-type header field has six valid values. The descriptions below describe when and how to use these values. For example alert for notifications that trigger a user interaction and background for notifications that deliver content in the background.
Some App Clips may need to schedule or receive notifications to provide value. Consider an App Clip that allows users to order food for delivery: By sending notifications, the App Clip informs the user about an upcoming delivery. If notifications are important for your App Clip’s functionality, enable it to schedule or receive notifications for up to 8 hours after each launch.
Remote notification only with content-available
Besides user interface notification, there is content-available notification that delivers notifications that wake your app and update it in the background. If your app’s server-based content changes infrequently or at irregular intervals, you can use background notifications to notify your app when new content becomes available. Read Pushing Background Updates to Your App
Testing push notification in simulator
We have been able to drag images to the Photos app in simulator for years, but new in Xcode 11.4 is the ability to drag push payload to simulator to simulate remote push notification.
All we have to do is create an apns file with Simulator Target Bundle key to specify our app, then drag to the simulator
Many of the simulator features can be controlled via xcrun simctl command line tool where you can change status bar time, battery info, start and stop certain simulators and send push with xcrun simctl push. This is very handy in case you want to automate things.
Test push notification easily with Push Hero
As iOS developers who need to test push notification a lot, I face this challenge. That’s why I made Push Hero as a native macOS application that allows us to reliably test push notification. It is written in pure Swift with all the new APNs specification in mind.
With Push Hero, we can setup multiple test scenario for different app. Each we can specify the authentication method we want, either with p8 token or p12 certificate based. There’s also input validation and hint helper that explains which field is needed and in which format, so you save time to work on your push testing instead.
New in latest version of Push Hero is the ability to send multiple pushes to multiple device tokens, which is the most frequent request. In the right history pane, there’s information about each request and response content, together with apns id information.
Also in Push Hero is the popup to show explanation for each field, and you need to consult Sending Notification Requests to APNs documentation as there are some specifications there. For example with VoIP push, the apns-topic header field must use your app’s bundle ID with .voip appended to the end. If you’re using certificate-based authentication, you must also register the certificate for VoIP services
Conclusion
Push notification continues to be important for iOS apps, and Apple has over the years improved and changed it for the better. This also means lots of knowledge to keep up with. Understanding provider APNs, setting up certificate and JSON Web Token key can be intimidating and take time.
Hopefully the above summary gets you more understanding into push notification, not only the history of changes in both client and provider API, but also some ways to test it easily.
However it seems view (UIButton or UILabel) size is the same, just the inner text increases in size. A workaround is to put view inside UIStackView so UIButton or UILabel can automatically changes size.
When your app moves to the background, the system calls your app delegate’s applicationDidEnterBackground(_:) method. That method has five seconds to perform any tasks and return. Shortly after that method returns, the system puts your app into the suspended state. For most apps, five seconds is enough to perform any crucial tasks, but if you need more time, you can ask UIKit to extend your app’s runtime.
You extend your app’s runtime by calling the beginBackgroundTask(withName:expirationHandler:) method. Calling this method gives you extra time to perform important tasks.
Use the BackgroundTasks framework to keep your app content up to date and run tasks requiring minutes to complete while your app is in the background. Longer tasks can optionally require a powered device and network connectivity.
Register launch handlers for tasks when the app launches and schedule them as required. The system will launch your app in the background and execute the tasks.
The main API for using this framework is the BGTaskScheduler . This API constantly monitors the system state such as battery level, background usage, and more, so it chooses the optimal time to run your tasks.
To use this API, you begin working when your app is on the foreground. You need to create Background task request. The framework provides an abstract class BGTask, you never use this task directly. Instead, the framework provides two concrete subclasses you can interact with: BGProcessingTask, for long running and maintenance tasks such backup and cleanup, and BGAppRefreshTask to keep your app up-to-date throughout the day.
When you create your background download or upload tasks with URLSession, you’re actually scheduling a download (or upload) with the ‘nsurlsessiond’ which is a daemon service that runs as a separate process.
It’s hard to see any iOS app which don’t use UITableView or UICollectionView, as they are the basic and important foundation to represent data. UICollectionView is very basic to use, yet a bit tedious for common use cases, but if we abstract over it, then it becomes super hard to customize. Every app is unique, and any attempt to wrap around UICollectionView will fail horribly. A sensable approach for a good abstraction is to make it super easy for normal cases, and easy to customize for advanced scenarios.
I’m always interested in how to make UICollectionView easier and fun to write and have curated many open sources here data source. Many of these data source libraries try to come up with totally different namings and complex paradigm which makes it hard to onboard, and many are hard to customize.
In its simplest form, what we want in a UICollectionView data source is cell = f(state), which means our cell representation is just a function of the state. We just want to set model to the cell, the correct cell, in a type safe manner.
Generic data source
The basic is to make a generic data source that sticks with a particular cell
1 2 3 4 5
classDataSource<T>: NSObject{ let items: [T] let configure: (T, UICollectionViewCell) -> Void let select: (UICollectionViewCell, IndexPath) -> Void }
This works for basic usage, and we can create multiple DataSource for each kind of model. The problem is it’s hard to subclass DataSource as generic in Swift and inheritance for ObjcC NSObject don’t work well.
Check for the types
Seeing the problem with generic data source, I’ve tried another approach with Upstream where it’s easier to declare sections and models.
This uses the Adapter pattern and we need to handle AdapterDelegate. To avoid the generic problem, this Adapter store items as Any, so we need to type cast all the time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
extensionProfileViewController: AdapterDelegate{ funcconfigure(model: Any, view: UIView, indexPath: IndexPath) { guardlet model = model as? Modelelse { return }
switch (model, view) { case (.avatar(let string), let cell asAvatarcell): cell.configure(string: string) case (.name(let name), let cell asNameCell): cell.configure(string: name) case (.header(let string), let view asHeaderView): view.configure(string: string) default: break } } }
The benefit is that we can easily subclass this Adapter manager to customize the behaviour, here is how to make accordion
SwiftUI comes in iOS 13 with a very concise and easy to use syntax. SwiftUI has good diffing so we just need to update our models so the whole content will be diffed and rendered again.
1 2 3 4 5 6 7 8 9 10 11 12
var body: some View { List { ForEach(blogs) { blog in VStack { Text(blog.name) } .onTap { print("cell was tapped") } } } }
SwiftUI style with diffing
I built DeepDiff before and it was used by many people. Now I’m pleased to introduce Micro which is a SwiftU style with DeepDiff powered so it performs fast diffing whenever state changes.
With Micro we can just use the familiar forEach to declare Cell, and the returned State will tell DataSource to update the UICollectionView.
Every time state is assigned, UICollectionView will be fast diffed and reloaded. The only requirement is that your model should conform to DiffAware with diffId so DeepDiff knows how to diff for changes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
let dataSource = DataSource(collectionView: collectionView) dataSource.state = State { ForEach(blogs) { blog in Cell<BlogCell>() { context, cell in cell.nameLabel.text = blog.name } .onSelect { context in print("cell at index \(context.indexPath.item) is selected") } .onSize { context in CGSize( width: context.collectionView.frame.size.width, height: 40 ) } } }
DataSource is completely overridable, if you want to customize any methods, just subclass DataSource, override methods and access its state.models
1 2 3 4 5 6
classCustomDataSource: DataSource{ overridefunccollectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let blog = state.models[indexPath.item] as? Blog print(blog) } }
This is iOS 13+ only, and the main components are the cellProvider acting as cellForItemAtIndexPath, and the snapshot for diffing. It also supports section.
1 2
let snapshot = NSDiffableDataSourceSnapshot<Section, Blog>() dataSource.apply(snapshot, animatingDifferences: animate)
In UITests, we can use press from XCUIElement to test drag and drop
1 2 3 4 5
let fromCat = app.buttons["cat1"].firstMatch let toCat = app.buttons["cat2"] let fromCoordinate = fromCat.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)) let toCoordinate = toCat.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: -0.5)) fromCoordinate.press(forDuration: 1, thenDragTo: toCoordinate)
and then take screenshot
1 2 3 4 5
let screenshot = XCUIScreen.main.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.lifetime = .keepAlways attachment.name = name add(attachment)
Screenshot capturing happens after the action, so it may be too late. One way is to inject launch arguments, like app.launchArguments.append("--dragdrop") to alter some code in the app.
We can also swizzle gesture recognizer to alter behavior
extensionUILongPressGestureRecognizer{ @objcvar uiTests_state: UIGestureRecognizer.State { let state = self.uiTests_state if state == .ended { return .changed } else { return state } } }
let originalSelector = #selector(getter: UILongPressGestureRecognizer.state) let swizzledSelector = #selector(getter: UILongPressGestureRecognizer.uiTests_state)
let originalMethod = class_getInstanceMethod(UILongPressGestureRecognizer.self, originalSelector)! let swizzledMethod = class_getInstanceMethod(UILongPressGestureRecognizer.self, swizzledSelector)!
let didAddMethod = class_addMethod(UILongPressGestureRecognizer.self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.
When the value of this property is true, Core Animation creates an implicit clipping mask that matches the bounds of the layer and includes any corner radius effects. If a value for the mask property is also specified, the two masks are multiplied to get the final mask value.
Mask layer
layer.cornerRadius, with or without layer.maskedCorners causes blending Use mask layer instead of layer.cornerRadius to avoid blending, but mask causes offscreen rendering
Instruments’ Core Animation Tool has an option called Color Offscreen-Rendered Yellow that will color regions yellow that have been rendered with an offscreen buffer (this option is also available in the Simulator’s Debug menu). Be sure to also check Color Hits Green and Misses Red. Green is for whenever an offscreen buffer is reused, while red is for when it had to be re-created.
Offscreen drawing on the other hand refers to the process of generating bitmap graphics in the background using the CPU before handing them off to the GPU for onscreen rendering. In iOS, offscreen drawing occurs automatically in any of the following cases:
Core Graphics (any class prefixed with CG) The drawRect() method, even with an empty implementation. CALayers with a shouldRasterize property set to YES. CALayers using masks (setMasksToBounds) and dynamic shadows (setShadow). Any text displayed on screen, including Core Text. Group opacity (UIViewGroupOpacity).
@abstract Sets the corner rounding method to use on the ASDisplayNode. There are three types of corner rounding provided by Texture: CALayer, Precomposited, and Clipping.
- ASCornerRoundingTypeDefaultSlowCALayer: uses CALayer's inefficient .cornerRadius property. Use this type of corner in situations in which there is both movement through and movement underneath the corner (very rare). This uses only .cornerRadius.
- ASCornerRoundingTypePrecomposited: corners are drawn using bezier paths to clip the content in a CGContext / UIGraphicsContext. This requires .backgroundColor and .cornerRadius to be set. Use opaque background colors when possible for optimal efficiency, but transparent colors are supported and much more efficient than CALayer. The only limitation of this approach is that it cannot clip children, and thus works best for ASImageNodes or containers showing a background around their children.
- ASCornerRoundingTypeClipping: overlays 4 separate opaque corners on top of the content that needs corner rounding. Requires .backgroundColor and .cornerRadius to be set. Use clip corners in situations in which is movement through the corner, with an opaque background (no movement underneath the corner). Clipped corners are ideal for animating / resizing views, and still outperform CALayer.
Generally, on iOS, pixel effects and Quartz / Core Graphics drawing are not hardware accelerated, and most other things are. The following things are not hardware accelerated, which means that they need to be done in software (offscreen): Anything done in a drawRect. If your view has a drawRect, even an empty one, the drawing is not done in hardware, and there is a performance penalty. Any layer with the shouldRasterize property set to YES. Any layer with a mask or drop shadow. Text (any kind, including UILabels, CATextLayers, Core Text, etc). Any drawing you do yourself (either onscreen or offscreen) using a CGContext.
For example, writing your own draw method with Core Graphics means your rendering will technically be done in software (offscreen) as opposed to being hardware accelerated like it is when you use a normal CALayer. This is why manually rendering a UIImage with a CGContext is slower than just assigning the image to a UIImageView.
if layer’s contents is nil or this contents has a transparent background, you just need to set cornerRadius. For UILabel, UITextView and UIButton, you can just set layer’s backgroundColor and cornerRadius to get a rounded corner. Note: UILabel’s backgroundColor is not its layer’s backgroundColor.
Here’s how it works: If you have an “Application Scene Manifest” in your Info.plist and your app delegate has a configurationForConnectingSceneSession method, the UIApplication won’t send background and foreground lifecycle messages to your app delegate. That means the code in these methods won’t run:
applicationDidBecomeActive applicationWillResignActive applicationDidEnterBackground applicationWillEnterForeground The app delegate will still receive the willFinishLaunchingWithOptions: and didFinishLaunchingWithOptions: method calls so any code in those methods will work as before.
UIApplication notifications
Notifications still trigger in iOS 13 if adopting SceneDelegate
Use foreground transitions to prepare your app’s UI to appear onscreen. An app’s transition to the foreground is usually in response to a user action. For example, when the user taps the app’s icon, the system launches the app and brings it to the foreground. Use a foreground transition to update your app’s UI, acquire resources, and start the services you need to handle user requests.
All state transitions result in UIKit sending notifications to the appropriate delegate object:
In iOS 13 and later—A UISceneDelegate object. In iOS 12 and earlier—The UIApplicationDelegate object.
You can support both types of delegate objects, but UIKit always uses scene delegate objects when they are available. UIKit notifies only the scene delegate associated with the specific scene that is entering the foreground. For information about how to configure scene support, see Specifying the Scenes Your App Supports.
keyWindow
Show most recent activeUIWindow
1
UIApplication.shared.keyWindow
This property holds the UIWindow object in the windows array that is most recently sent the makeKeyAndVisible() message.
For the view controller’s root view, the insets account for the status bar, other visible bars, and any additional insets that you specified using the additionalSafeAreaInsets property of your view controller. For other views in the view hierarchy, the insets reflect only the portion of the view that is covered. For example, if a view is entirely within the safe area of its superview, the edge insets in this property are 0.
Use UICollectionView.contentInsetAdjustmentBehavior
Nested view
For UICollectionView inside Cell inside UICollectionView, its insets is 0, but its parent parent is correct, which is the original cell
viewWillAppear: safeAreaInsets is not set to collectionView viewDidAppear: safeAreaInsets is set to collectionView and cells, but not to nested collectionView
In viewSafeAreaInsetsDidChange, invalidate outer and nested collectionViewLayout
But it’s not, it is because of when safeAreaInsets is available, and how it is passed to nested view
When invalidating collection view layout with custom UIPresentationController, alongsideTransition is called twice, the first time with old safeAreaInsets, and the second time with latest safeAreaInsets
This method is called after the prepare(forCollectionViewUpdates:) method and before the finalizeCollectionViewUpdates() method for any decoration views that are about to be inserted. Your implementation should return the layout information that describes the initial position and state of the view. The collection view uses this information as the starting point for any animations. (The end point of the animation is the view’s new location in the collection view.) If you return nil, the layout object uses the item’s final attributes for both the start and end points of the animation.
The default implementation of this method returns nil.
Although the doc says “The default implementation of this method returns nil”, calling super.initialLayoutAttributesForAppearingDecorationElement gives somehow implicit animation. The workaround is to explicitly return nil
/// Act as DataSource and Delegate for UICollectionView, UITableView openclassAdapter: NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UITableViewDataSource, UITableViewDelegate{