So, straight to the problem:
I've created a custom UIViewControllerTransitioningDelegate that I use to animate a view from one view controller, to full-screen in another view controller. Im doing this by creating UIViewControllerAnimatedTransitioning-objects that animate the presented view's frame. And it works great! Except when I try to adjust the additionalSafeAreaInsets of the view controller owning the view during dismissal...
It looks like this property is not accounted for when I'm trying to animate the dismissal of the view controller and its view. It works fine during presentation.
So, what I want is: use additionalSafeAreaInsets to diminish the effect of the safe area during animation, by setting additionalSafeAreaInsets to the "inverted" values of the safe area. So that the effective safe area starts at 0 and "animates" to the expected value during presentation, and starts at expected value and "animates" to 0 during dismissal.
(I'm quoting "animates", since its actually the view's frame that is animated. But UIKit/Auto Layout use these properties when calculating the frames)
Any thoughts on how to battle this issue is great welcome!
The code for the custom UIViewControllerTransitioningDelegate is provided as an attachment.
FullScreenTransitionManager.swift
Post
Replies
Boosts
Views
Activity
To me it looks like the safe area of a view is not updated after the owning view controller's .viewWillDisappear() method is called.
Is this intended or a bug in the framework?
The issue is easily visualised by creating a custom UIViewControllerTransitioningDelegate that animates a smaller view in one view controller, to full screen size in another (constrained to safe areas). Then the safe areas will expand as the present animation goes on (as expected), but will not shrink as the dismiss animation goes on (not expected!). The expected behaviour would be that the safe area expands during the present animation, and shrinks during the dismiss animation.
The gif below shows the unexpected behaviour. The grey area of the presented view controller is the safe area.
I've attached the code I used to visualise this problem. ViewController.swift presents MyViewController.swift using FullScreenTransitionManager.swift
FullScreenTransitionManager.swift
MyViewController.swift
ViewController.swift
In short - when construction a custom SwiftUI View with a ViewModel ("MyView"/"MyViewModel") like this:
struct ContentView: View {
var body: some View {
MyView(viewModel: MyViewModel())
}
}
Why is this:
struct MyView: View {
@StateObject var viewModel: MyViewModel
var body: some View {
Text("Hello world!")
}
}
Not the same as this:
struct MyView: View {
@StateObject var viewModel: MyViewModel
init(viewModel: MyViewModel) {
self._viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
Text("Hello world!")
}
}
When debugging, it looks like the second option causes new instances of MyViewModel to be created every time MyView is reconstructed by SwiftUI. But in the first option, the StateObject wrapper seems to do its job, and makes sure only one instance of MyViewModel is created for all reconstructions of MyView.
Is there some extra SwiftUI magic applied to using the View's default memberwise initializer VS a custom one? Perhaps by the ViewBuilder?
Attachment: A simple example app, MyApp.swift, to see the behaviour in action.
MyApp.txt
I've been playing around with the awesome UIViewPropertyAnimator, but are observing something during scrubbing I don't quite understand.
Linearly scrubbing a UIViewPropertyAnimator configured with "fast" timing parameters and/or low animation duration, causes the animated properties to snap between (larger) values at the start of scrubbing.
This can be made visible with a simple bottom sheet example (recorded on iPhone 14 Pro with iOS 16.4.1):
Small/"fast" timing parameters:
Large/"slow" timing parameters:
The UIViewPropertyAnimator is instantiated as follows (the issue also observed with other initializers):
// duration set to 0.25 gives the unwanted snapping at the start of scrubbing:
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1)
// duration set to 1.25 gives the expected smooth scrubbing.
UIViewPropertyAnimator(duration: 1.25, dampingRatio: 1)
Any ideas about what's going on here? I would expect the scrubbing to be smooth independent of what parameters are given? It's especially odd that the snapping only happens at the start of the scrubbing...
Also attached the full example code:
BottomSheetAnimationViewController.txt
There seem to be an issue with the DeviceCheck Framework where in rare cases the public key (ECC P-256) embedded inside the attestation object returned from DCAppAttestService.attestKey(_:clientDataHash:completionHandler:) has X and Y coordinates with mismatching length. Sometimes X or Y has 31 bytes instead of the expected 32 bytes.
This can easily be reproduced by generating and attesting multiple keys using DCAppAttestService.generateKey(completionHandler:) and DCAppAttestService.attestKey(_:clientDataHash:completionHandler:). Every now and then the public key embedded inside the attestation object has X and Y coordinates with mismatching length (number of bytes).
Added a Swift snippet at the bottom that shows example on how to generate and detect this.
I would expect the ECC P-256 public key X and Y coordinates to always be 32 bytes long. As mentioned in the Web Authentication spec for example.
I've attached an example attestation object (in base64 encoded CBOR) that has an embedded public key with mismatching X and Y coordinate length (Y is 31 bits, and not the expected 32 bits). The file was generated using the Swift snippet below. The snippet was built using Xcode 14.3 (14E222b) and ran on iPhone XR with iOS 15.7.1 (19H117).
A feedback ticket has also been submitted regarding this issue: FB12235865
Swift snippet to generate and check attestation objects:
import DeviceCheck
import CryptoKit
import SwiftCBOR // https://github.com/valpackett/SwiftCBOR
func generateAttestationObjects() {
for i in 0..<1000 {
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(i)) {
DCAppAttestService.shared.generateKey { keyId, error in
guard let keyId else {
print("\(i): Failed to generate key: \(error)")
return
}
print("\(i): Generated keyId: \(keyId)")
DCAppAttestService.shared.attestKey(
keyId,
clientDataHash: Data(hex: "01020304")!
) { attestationObject, error in
guard let attestationObject else {
print("\(i): Failed to get attestation: \(error)")
return
}
do {
let attestationObjectBytes = [UInt8](attestationObject)
if case let .map(decodedAttestationObject) = try CBOR.decode(attestationObjectBytes) {
print("\(i): Successfully decoded Attestation object (CBOR)")
if case let .byteString(authData) = decodedAttestationObject["authData"] {
let attestedCredentialData = [UInt8](authData.dropFirst(37))
let credentialIdLengthBuffer = [UInt8](attestedCredentialData[16..<18])
let credentialIdLength = Int(credentialIdLengthBuffer.reversed().withUnsafeBytes { $0.load(as: UInt16.self) })
let credentialId = [UInt8](attestedCredentialData[18..<(18 + credentialIdLength)])
let credentialPublicKeyBuffer = [UInt8](attestedCredentialData.dropFirst(18 + credentialIdLength))
if let decodedCredentialPublicKey = try CBOR.decode(credentialPublicKeyBuffer) {
if
case let .byteString(xCoordinateBuffer) = decodedCredentialPublicKey[-2],
case let .byteString(yCoordinateBuffer) = decodedCredentialPublicKey[-3]
{
let xCoordinateLength = xCoordinateBuffer.count
let yCoordinateLength = yCoordinateBuffer.count
if xCoordinateLength != yCoordinateLength {
print("\(i): X/Y Coordinate length mismatch! X: \(xCoordinateLength), Y: \(yCoordinateLength)")
} else if xCoordinateLength != 32 || yCoordinateLength != 32 {
print("\(i): X/Y Coordinate length mismatch! X: \(xCoordinateLength), Y: \(yCoordinateLength)")
} else {
print("\(i): X/Y Coordinates OK")
}
}
}
}
}
} catch {
print("\(i): Error decoding Attestation object (CBOR): \(error)")
}
}
}
}
}
}
An attestation object with a embedded public key with mismatching X and Y coordinate length (base64 encoded CBOR):
appattest-object-mismatching-x-y-base64.txt
In an attempt to expose the capabilities of NSAttributedString in combination with UITextView to the world of SwiftUI (specifically the ability to render basic HTML), I've wrapped UITextView in a UIViewRepresentable and used that in a custom SwiftUI View.
But I'm seeing some issues I can't really explain... So I would love to get a deeper understanding of what's going on. And possible also find a way to fix these issues in an appropriate way.
This is how it goes:
UIViewRepresentable wrapping UITextView to display NSAttributedString in the context of SwiftUI
import SwiftUI
struct AttributedText: UIViewRepresentable {
private let attributedString: NSAttributedString
init(_ attributedString: NSAttributedString) {
self.attributedString = attributedString
}
func makeUIView(context: Context) -> UITextView {
let uiTextView = UITextView()
// Make it transparent so that background Views can shine through.
uiTextView.backgroundColor = .clear
// For text visualisation only, no editing.
uiTextView.isEditable = false
// Make UITextView flex to available width, but require height to fit its content.
// Also disable scrolling so the UITextView will set its `intrinsicContentSize` to match its text content.
uiTextView.isScrollEnabled = false
uiTextView.setContentHuggingPriority(.defaultLow, for: .vertical)
uiTextView.setContentHuggingPriority(.defaultLow, for: .horizontal)
uiTextView.setContentCompressionResistancePriority(.required, for: .vertical)
uiTextView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return uiTextView
}
func updateUIView(_ uiTextView: UITextView, context: Context) {
uiTextView.attributedText = attributedString
}
}
Used in a custom HTML SwiftUI View
import SwiftUI
struct HTML: View {
private let bodyText: String
init(_ bodyText: String) {
self.bodyText = bodyText
}
var body: some View {
AttributedText((try? NSAttributedString(
data: """
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style type="text/css">
body {
font: -apple-system-body;
color: white;
}
</style>
</head>
<body>
\(bodyText)
</body>
</html>
""".data(using: .utf8)!,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: NSUTF8StringEncoding,
],
documentAttributes: nil
)) ?? NSAttributedString(string: bodyText))
}
}
Put together in a simple SwiftUI app
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
ScrollView {
HTML("""
<p>This is a paragraph</p>
<ul>
<li>List item one</li>
<li>List item two</li>
</ul>
""")
}
.navigationTitle("HTML in SwiftUI")
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Now, when I build and run the simple SwiftUI app shown above, it renders just fine, but there is a lot of log entries similar to "=== AttributeGraph: cycle detected through attribute 24504 ===". In addition to that, the navigation title bugs out when I scroll up. It also seems like SwiftUI is not able to detect changes to the HTML View, and does not re-evaluate its body if I re-create HTML with a new bodyText (even though its structural identity is preserved).
When I use Instruments to inspect SwiftUI View body invocations, I can see that initiating the inline HTML styled NSAttributedString in the HTML View's body takes several milliseconds (not too surprising as it calls into WebKit stuff?), resulting in HTML.body taking more than 15 milliseconds to complete. Which is a lot more than if i just instantiated a "pure" text string using e.g the NSAttributedString(string:) initialiser.
The initial render also seem to call HTML.body twice, a second time after calling the body of some View labeled "RootModifier" (Maybe the invocation of HTML.body took too long, and SwiftUI tries again?).
Now, I acknowledge that all these signs yell "do not call computational heavy stuff inside a View's body!", but still, I would love to understand why SwiftUI complains about cycles in its AttributeGraph (as I can't really see any), and why SwiftUI does not re-evaluate HTML's body if I re-create HTML with a new bodyText (as HTML's initialiser is clearly called with a new and different bodyText value).
I could also just completely drop the custom HTML SwiftUI View, and just use the AttributedText UIViewRepresentable directly. And then fully manage instances of HTML styled NSAttributedStrings in my model layer (and not instantiate them as part of some custom SwiftUI View). But that would remove some of the abstraction and readability of having a dedicated SwiftUI View for rendering HTML. So any suggestions on how to create such an abstraction/SwiftUI View would be greatly appreciated as well!
Is it possible?
The original key was generated and stored in the Keychain using the following code:
func generateSecureEnclaveProtectedSecKey(withTag tag: Data) throws -> SecKey {
var error: Unmanaged<CFError>?
let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage],
&error
)!
let attributes = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrCanSign as String: true,
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: tag,
kSecAttrAccessControl as String: accessControl,
] as [String: Any],
] as [String: Any]
let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error)!
return privateKey
}
Then I wanted to use the strongly typed interface of CryptoKit, so I naively tried to get a hold of the existing key as follows (querying for kSecReturnPersistentRef and not kSecReturnRef):
func getSecureEnclaveProtectedCryptoKitKey(fromSecureEnclaveProtectedSecKeyWithTag tag: Data) throws -> SecureEnclave.P256.Signing.PrivateKey {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecReturnPersistentRef as String: true,
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
let keyData = item as! CFData
return try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData as Data)
}
But that resulted in:
Error Domain=CryptoTokenKit Code=-3 "corrupted objectID detected" UserInfo={NSLocalizedDescription=corrupted objectID detected}
Since this is a Secure Enclave protected key, it is not possible to use SecKeyCopyExternalRepresentation (or query for kSecReturnData), but perhaps there is another way to convert a SecKey object to a SecureEnclave.P256.Signing.PrivateKey?
The other way around seem to be possible using the answers to this blog post: https://developer.apple.com/forums/thread/728314