How to handle UICollectionView reloadData with selected index path
Issue #434
When calling collectionView.reloadData()
, selected indexpath stays the same, but be aware that order of data may have changed
1 | let selectedData = ... |
Issue #434
When calling collectionView.reloadData()
, selected indexpath stays the same, but be aware that order of data may have changed
1 | let selectedData = ... |
Issue #424
One confusing point here that people often do not realize is that even though the custom token itself expires after one hour, a modern client SDK authenticated with that custom token will stay authenticated beyond that hour! What happens under the hood is that the custom token is sent to the Firebase Auth service in exchange for an ID token and refresh token pair which are used to keep the client SDK authenticated
As with custom tokens, ID tokens are short-lived JWTs, lasting for just one hour. In order to allow end users to stay logged in for more than one hour, the modern SDKs transparently refresh a user’s ID token on your behalf using a refresh token
If your app includes a custom backend server, ID tokens can and should be used to communicate securely with it. Instead of sending requests with a user’s raw uid which can be easily spoofed by a malicious client, send the user’s ID token which can be verified via a Firebase Admin SDK
When a user or device successfully signs in, Firebase creates a corresponding ID token that uniquely identifies them and grants them access to several resources, such as Firebase Realtime Database and Cloud Storage. You can re-use that ID token to identify the user or device on your custom backend server. To retrieve the ID token from the client, make sure the user is signed in and then get the ID token from the signed-in user:
Issue #422
To constrain views outside to elements inside UICollectionViewCell
, we can use UILayoutGuide
.
Need to make layout guide the same constraints as the real elements
1 | let imageViewGuide = UILayoutGuide() |
1 | NSLayoutConstraint.on([ |
Issue #421
1 | private func maskCvcIfAny() { |
where tag is in STPPaymentCardTextFieldViewModel.h
1 | typedef NS_ENUM(NSInteger, STPCardFieldType) { |
Also, need to check accessibilityLabel
in STPPaymentCardTextField.m
1 | - (NSString *)defaultCVCPlaceholder { |
Issue #414
Codable
is awesome, but sometimes we just need to quickly get value in a deepy nested JSON. In the same way I did for Dart How to resolve deep json object in Dart, let’s make that in Swift.
See https://github.com/onmyway133/Omnia/blob/master/Sources/Shared/JSON.swift
1 | public func resolve<T>(_ jsonDictionary: [String: Any], keyPath: String) -> T? { |
So we can just resolve via key path
1 | class JSONTests: XCTestCase { |
Issue #412
When the value of this property is true, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content. Shadow effects and any filters in the filters property are rasterized and included in the bitmap. However, the current opacity of the layer is not rasterized. If the rasterized bitmap requires scaling during compositing, the filters in the minificationFilter and magnificationFilter properties are applied as needed.
In the class PinView: UIView
1 | isOpaque = true |
When your app needs to draw something on the screen, the GPU takes your layer hierarchy (UIView is just a wrapper on top of CALayer, which in the end are OpenGL textures) and applies one by one on top of each other based on their x,y,z position. In regular rendering, the whole operation happens in special frame buffers that the display will directly read for rendering on the screen, repeating the process at a rate around 60 times per second.
Of course the process have some drawbacks as well. The main one is that offscreen rendering requires a context switch (GPU has to change to a different memory area to perform the drawing) and then copying the resulting composited layer into the frame buffer. Every time any of the composited layers change, the cache needs to be redrawn again. This is why in many circumstances offscreen rendering is not a good idea, as it requires additional computation when need to be rerendered. Besides, the layer requires extra video memory which of course is limited, so use it with caution.
Issue #411
1 | class ViewController: UIViewController, UICollectionViewDropDelegate, UICollectionViewDragDelegate { |
Issue #402
I want to test if a date has passed another date
1 | let base = Date(timeIntervalSince1970: 1567756697) |
My hasPassed
is using Calendar.current
1 | func minuteSinceMidnight(date: Date) -> MinuteSinceMidnight { |
But the minute is always having timezone applied. Even if I try with DateComponents
1 | func minuteSinceMidnight(date: Date) -> MinuteSinceMidnight { |
As long as I use Calendar
, it always has timezone applied.
Checking this time interval 1567756697
on https://www.epochconverter.com/
Assuming that this timestamp is in seconds:
GMT: Friday, September 6, 2019 7:58:17 PM
Your time zone: Friday, September 6, 2019 9:58:17 PM GMT+02:00 DST
Because I have GMT+2, there will always be 2 hours offset. This works in app, but not in test because of the way I construct Date with time interval.
One way is to have test data using string construction, and provide timezone to DateFormatter
1 | let formatter = ISO8601DateFormatter() |
Another way is to have a fixed timezone for Calendar
1 | var calendar = Calendar.current |
Another way is to adjust existing date
1 | calendar.date(bySettingHour: 20, minute: 02, second: 00, of: Date() |
Issue #395
Prefer static enum to avoid repetition and error. The Log should have methods with all required fields so the call site is as simple as possible. How to format and assign parameters is encapsulated in this Analytics.
1 | import Foundation |
Issue #387
Google Analytics is shutting down. From Firebase Analytics console, we can choose to upgrade to Google Analytics, no code change is needed.
https://support.google.com/firebase/answer/9167112?hl=en
In October 2019, we will start to sunset Google Analytics mobile-apps reporting based on the Google Analytics Services SDKs for Android and iOS.
https://firebase.googleblog.com/2019/07/firebase-google-analytics-upgrade.html
Thanks to our continued partnership with Google Analytics, you can now upgrade your Firebase projects to the next generation of app analytics!
https://www.e-nor.com/blog/google-analytics/google-analytics-unifies-app-and-website-measurement
Google Analytics team has officially launched a new type of GA properties called “App + Web” to open public beta
It is a new GA property type that allows you to combine app and web data for unified reporting and analysis
Over the coming weeks, those who have existing Firebase projects will be able to upgrade your projects to the next generation Google Analytics experience as follows:
Issue #386
When it comes to right
and bottom
side, we should use negative values, and use lessThan
, as it means less than a negative value
Issue #385
1 | + (BOOL)isJailbroken { |
Issue #377
OneSignal is an alternative for Parse for push notifications but the sdk has many extra stuff and assumptions and lots of swizzling.
We can just use Rest to make API calls. From https://github.com/onmyway133/Dust
Every official push notification SDK can do many things
OneSignal
1 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool { |
Here is the implementation
1 | import UIKit |
1 | import Foundation |
1 | import Foundation |
Issue #376
https://github.com/onmyway133/Omnia/blob/master/Sources/Shared/Debouncer.swift
1 | import Foundation |
1 | import XCTest |
Issue #375
1 | final class LifecyclerHandler { |
1 | private let lifecycleHandler = LifecyclerHandler() |
Issue #374
Add accessibilityIdentifier
to the parent view of GMSMapView
. Setting directly onto GMSMapView
has no effect
1 | accessibilityIdentifier = "MapView" |
1 | let map = app.otherElements.matching(identifier: "MapView").element(boundBy: 0) |
Need to enable accessibility
1 | mapView.accessibilityElementsHidden = false |
Can’t use pinch to zoom out with UITests, so need to mock location !!!
1 | map().pinch(withScale: 0.05, velocity: -1) |
Need to use gpx to mock to preferred location
1 | let map = app.otherElements[Constant.AccessibilityId.mapView.rawValue] |
Try isAccessibilityElement = true
for PinView
, can’t touch!!
Use coordinate, can’t touch !!
1 | let coordinate = pin.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) |
Try traversing all the pins, can’t touch
1 | Array(0..<pins.count).forEach { |
When po app.otherElements
, coordinates are outside screen
1 | Other, {{1624.0, 1624.0}, {30.0, 30.0}}, identifier: 'pin', label: 'Hello world' |
My PinView
has isHittable
being false, no matter how I use coordinate or enable it. It can’t be touched.
Go to Xcode -> Open Developer Tool -> Accessibility Inspector to inspect our app in iOS simulator
It turns out that if I do
1 | po app.buttons |
It shows all the GMSMarker, but with identifier
having class name MyApp.MyStopMarker
, so just need to use buttons
1 | extension NSPredicate { |
Updated at 2021-01-26 09:47:41
Issue #373
UIButton.contentEdgeInsets
does not play well with Auto Layout, we need to use intrinsicContentSize
1 | final class InsetButton: UIButton { |
Issue #371
Scrolling UIScrollView
is used in common scenarios like steps, onboarding.
From iOS 11, UIScrollView has contentLayoutGuide
and frameLayoutGuide
https://developer.apple.com/documentation/uikit/uiscrollview/2865870-contentlayoutguide
Use this layout guide when you want to create Auto Layout constraints related to the content area of a scroll view.
https://developer.apple.com/documentation/uikit/uiscrollview/2865772-framelayoutguide
Use this layout guide when you want to create Auto Layout constraints that explicitly involve the frame rectangle of the scroll view itself, as opposed to its content rectangle.
I found out that using contentLayoutGuide
and frameLayoutGuide
does not work in iOS 11, when swiping to the next page, it breaks the constraints. iOS 12 works well, so we have to check iOS version
Let the contentView
drives the contentSize
of scrollView
1 | import UIKit |
1 | extension UILayoutGuide { |
Issue #369
translatesAutoresizingMaskIntoConstraints
set to false
UIStackView
stackview.alignment = .center
if you see UIStackView
trying to set same trailing or leading edges for its subviewsUIStackView
1 | imageView.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: .vertical) |
https://stackoverflow.com/a/58666480/1418457
Issue #368
See https://github.com/onmyway133/Omnia/blob/master/Sources/iOS/NSLayoutConstraint.swift
1 | extension NSLayoutConstraint { |
1 | extension UILayoutGuide { |
1 | extension UIView { |
Issue #365
1 | import WebKit |
Issue #364
AppFlowController.swift
1 | import UIKit |
AppDelegate.swift
1 |
|
Issue #362
1 | let tapGR = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) |
We need to use lazy
instead of let
for gesture to work
1 | lazy var tapGR = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) |
Issue #356
StripeHandler.swift
From Stripe 16.0.0 https://github.com/stripe/stripe-ios/blob/master/CHANGELOG.md#1600-2019-07-18
Migrates STPPaymentCardTextField.cardParams property type from STPCardParams to STPPaymentMethodCardParams
1 | final class StripeHandler { |
https://stripe.com/docs/payments/payment-intents/creating-payment-intents
When using automatic confirmation, create the PaymentIntent at the beginning of the checkout process. When using manual confirmation, create the PaymentIntent after collecting payment information from the customer using Elements or our iOS and Android SDKs. For a detailed comparison on the automatic and manual confirmation flows, see accepting one-time payments.
Pass the confirmed Payment Intent client secret from the previous step to STPPaymentHandler handleNextActionForPayment. If the customer must perform 3D Secure authentication to complete the payment, STPPaymentHandler presents view controllers using the STPAuthenticationContext passed in and walks them through that process. See Supporting 3D Secure Authentication on iOS to learn more.
1 | MyAPIClient.createAndConfirmPaymentIntent(paymentMethodId: paymentMethodId) { result in |
There is Setup intents https://stripe.com/docs/payments/cards/reusing-cards#saving-cards-without-payment for saving cards
Use the Setup Intents API to authenticate a customer’s card without making an initial payment. This flow works best for businesses that want to onboard customers without charging them right away:
Pass the STPSetupIntentParams object to the confirmSetupIntent method on a STPPaymentHandler sharedManager. If the customer must perform additional steps to complete the payment, such as authentication, STPPaymentHandler presents view controllers using the STPAuthenticationContext passed in and walks them through that process. See Supporting 3D Secure Authentication on iOS to learn more.
1 | let setupIntentParams = STPSetupIntentParams(clientSecret: clientSecret) |
In STPPaymentHandler.m
1 | - (BOOL)_canPresentWithAuthenticationContext:(id<STPAuthenticationContext>)authenticationContext { |
STPSetupIntentConfirmParams.useStripeSDK
A boolean number to indicate whether you intend to use the Stripe SDK’s functionality to handle any SetupIntent next actions.
If set to false, STPSetupIntent.nextAction will only ever contain a redirect url that can be opened in a webview or mobile browser.
When set to true, the nextAction may contain information that the Stripe SDK can use to perform native authentication within your app.
1 | let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: clientSecret) |
Issue #353
When using STPPaymentCardTextField
from stripe-ios, the default behavior is when we touch outside to dismiss keyboard, it checks and focus on number text field is it is invalid
STPPaymentCardTextField.m
1 | - (STPFormTextField *)currentFirstResponderField { |
Then it calls [self.numberField becomeFirstResponder];
is validation on number fails
1 | typedef void (^STPLayoutAnimationCompletionBlock)(BOOL completed); |
Be aware to use isUserInteractionEnabled
on STPPaymentCardTextField
as that can resign first responder when set to true
and become first responder when set to false
Issue #350
Read Authenticate with Firebase on iOS using a Phone Number
Info.plist
1 | <key>FirebaseAppDelegateProxyEnabled</key> |
Enable Capability -> Background mode -> Remote notification
AppDelegate.swift
1 | import Firebase |
Firebase push message looks like
1 | ▿ 1 element |
To disable captcha during testing
1 | Auth.auth().settings?.isAppVerificationDisabledForTesting = true |
Issue #347
Add a hidden UITextField
to view hierarchy, and add UITapGestureRecognizer
to activate that textField.
Use padding string with limit to the number of labels, and prefix to get exactly n characters.
DigitView.swift
1 | import UIKit |
DigitHandler.swift
1 | final class DigitHandler: NSObject { |
Issue #346
We have FrontCard that contains number and expiration date, BackCard that contains CVC. CardView is used to contain front and back sides for flipping transition.
We leverage STPPaymentCardTextField
from Stripe for working input fields, then CardHandler
is used to parse STPPaymentCardTextField
content and update our UI.
For masked credit card numbers, we pad string to fit 16 characters with ●
symbol, then chunk into 4 parts and zip with labels to update.
For flipping animation, we use UIView.transition
with showHideTransitionViews
BackCard.swift
1 | import UIKit |
FrontCard.swift
1 | import UIKit |
CardView.swift
1 | import UIKit |
CardHandler.swift
1 | import Foundation |
String+Extension.swift
1 | extension String { |
Updated at 2020-07-12 08:43:21
Issue #345
UIButton
with system type has implicit animation for setTitle(_:for:)
Use this method to set the title for the button. The title you specify derives its formatting from the button’s associated label object. If you set both a title and an attributed title for the button, the button prefers the use of the attributed title over this one.
At a minimum, you should set the value for the normal state. If a title is not specified for a state, the default behavior is to use the title associated with the normal state. If the value for normal is not set, then the property defaults to a system value.
1 | UIView.performWithoutAnimation { |
Issue #344
addSubview can trigger viewDidLayoutSubviews, so be careful to just do layout stuff in viewDidLayoutSubviews
This method establishes a strong reference to view and sets its next responder to the receiver, which is its new superview.
Views can have only one superview. If view already has a superview and that view is not the receiver, this method removes the previous superview before making the receiver its new superview.
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. However, this method being called does not indicate that the individual layouts of the view’s subviews have been adjusted. Each subview is responsible for adjusting its own layout.
Your view controller can override this method to make changes after the view lays out its subviews. The default implementation of this method does nothing.