How to simplify UIApplication life cycle observation in iOS
Issue #375
1 | final class LifecyclerHandler { |
1 | private let lifecycleHandler = LifecyclerHandler() |
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 #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 #361
1 | protocol Task {} |
1 | run { |
Issue #360
Given a streaming service
1 | service Server { |
To get a response list in Swift, we need to do observe stream, which is a subclass of ClientCallServerStreaming
1 | func getUsers(roomId: String, completion: @escaping (Result<[User], Error>) -> Void) { |
This can get repetitive very fast. To avoid the duplication, we can make a generic function
1 | import SwiftGRPC |
Since swift-grpc generates very concrete structs, we need to use generic. The difference is the Streaming
class and Response
struct
1 | func getUsers(roomId: String, completion: @escaping (Result<[User], Error>) -> Void) { |
1 | import SwiftGRPC |
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 #355
1 | final class CurrencyFormatter { |
1 | class CurrencyFormatterTests: XCTestCase { |
Issue #354
In Construction, we have a build
method to apply closure to inout
struct.
We can explicitly define that with withValue
1 | func withValue<T>(_ value: T, closure: (inout T) -> Void) -> T { |
So we can modify Protobuf structs easily
1 | user.book = withValue(Book()) { |
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.
Issue #340
https://nshipster.com/formatter/#datecomponentsformatter
Results in no padding 0
1 | func format(second: TimeInterval) -> String? { |
1 | func format(minute: Int) -> String { |
Issue #337
Normally we just present from any UIViewController
in any UINavigationController
in UITabBarController
and it will present over tabbar
1 | present(detailViewController, animated: true, completion: nil) |
If we have animation with UIViewPropertyAnimator
, then we can implement UIViewControllerAnimatedTransitioning and interruptibleAnimator(using:)
The methods in this protocol let you define an animator object, which creates the animations for transitioning a view controller on or off screen in a fixed amount of time. The animations you create using this protocol must not be interactive. To create interactive transitions, you must combine your animator object with another object that controls the timing of your animations.
Implement this method when you want to perform your transitions using an interruptible animator object, such as a UIViewPropertyAnimator object. You must return the same animator object for the duration of the transition.
For more fine-grained control, we can have UIPresentationController
From the time a view controller is presented until the time it is dismissed, UIKit uses a presentation controller to manage various aspects of the presentation process for that view controller. The presentation controller can add its own animations on top of those provided by animator objects, it can respond to size changes, and it can manage other aspects of how the view controller is presented onscreen.
A lazy approach is to present without animation and do animation after
1 | present(detailViewController, animated: false, completion: { |
If we don’t want to involve UIViewController
then we can work on UIView
level. This way we can animate hiding tab bar. Any UIViewController
within UITabBarController
has tabBarController
1 | let animator = UIViewPropertyAnimator() |
Issue #336
Use NSPopUpButton
var pullsDown: Bool
A Boolean value indicating whether the button displays a pull-down or pop-up menu.
func addItem(withTitle: String)
Adds an item with the specified title to the end of the menu.
Should disable pullsDown
if we want to set title automatically and not scale button for title
Issue #335
This is useful when we want to get the first meaningful line in a big paragraph
1 | let scanner = Scanner(string: text) |
Issue #334
NSSecureCoding has been around since iOS 6 and has had some API changes in iOS 12
A protocol that enables encoding and decoding in a manner that is robust against object substitution attacks.
https://developer.apple.com/documentation/foundation/nscoder/2292924-decodeobject
If the coder responds true to requiresSecureCoding, then the coder calls failWithError(_:) in either of the following cases:
The class indicated by cls doesn’t implement NSSecureCoding.
The unarchived class doesn’t match cls, nor do any of its superclasses.
If the coder doesn’t require secure coding, it ignores the cls parameter and does not check the decoded object.
The class must subclass from NSObject
and conform to NSSecureCoding
1 | class Note: NSObject, NSSecureCoding { |
First, we need to serialize to Data, then use EasyStash for easy persistency
1 | do { |
Then we can use unarchiveTopLevelObjectWithData
to unarchive array
1 | do { |
Note that for UUID, NSCoding seems to convert to UUID instead of String
1 | let id = aDecoder.decodeObject( |
Issue #333
In a traditional pager with many pages of content, and a bottom navigation with previous and next button. Each page may have different content, and depending on each state, may block the next button.
The state of next button should state in real time depending on state in each page content, and when user moves back and forth between pages, the state of next button should be reflected as well.
We might have
1 | extension ViewController: BottomNavigationDelegate { |
The indirect communications between each page, bottom navigation and ViewController get complicated and out of hands very quickly.
This is a perfect problem for Rx to solve. If we look closely, the state of next button is a derivative of current index, how many items selected in preferences, valid form and agreement status.
1 | class BottomNavigation { |
Issue #332
From moveItem(at:to:)
Moves an item from one location to another in the collection view.
After rearranging items in your data source object, use this method to synchronize those changes with the collection view. Calling this method lets the collection view know that it must update its internal data structures and possibly update its visual appearance. You can move the item to a different section or to a new location in the same section. The collection view updates the layout as needed to account for the move, animating cells into position in response.
When inserting or deleting multiple sections and items, you can animate all of your changes at once using the performBatchUpdates(_:completionHandler:) method.
1 | notes.swapAt(index, 0) |
There may be unknown reasons or bug that make other cells stay in incorrect state. The fix is to reload the rest cells
1 | let set = Set((1..<notes.count).map({ $0.toIndexPath() })) |
Issue #331
From NSSegmentedControl
The features of a segmented control include the following:
A segment can have an image, text (label), menu, tooltip, and tag.
A segmented control can contain images or text, but not both.
1 | let languageMenu = NSMenu(title: "") |
Issue #330
When adding NSTextView
in xib, we see it is embedded under NSClipView
. But if we try to use NSClipView
to replicate what’s in the xib, it does not scroll.
To make it work, we can follow Putting an NSTextView Object in an NSScrollView and How to make scrollable vertical NSStackView to make our ScrollableInput
For easy Auto Layout, we use Anchors for UIScrollView
.
Things worth mentioned for vertical scrolling
1 | textContainer.heightTracksTextView = false |
1 | class ScrollableInput: NSView { |
NSTextView.scrollableTextView()
Updated at 2020-12-31 05:43:41
Issue #329
Firstly, to make UIStackView scrollable, embed it inside UIScrollView. Read How to embed UIStackView inside UIScrollView in iOS
It’s best to listen to keyboardWillChangeFrameNotification
as it contains frame changes for Keyboard in different situation like custom keyboard, languages.
Posted immediately prior to a change in the keyboard’s frame.
1 | class KeyboardHandler { |
To make scrollView scroll beyond its contentSize
, we can change its contentInset.bottom
. Another way is to add a dummy view with certain height to UIStackView
and alter its NSLayoutConstraint
constant
We can’t access self
inside init, so it’s best to have setup function
1 | func setup() { |
Convert Notification
to a convenient Info
struct
1 | func convert(notification: Notification) -> Info? { |
Then we can compare with UIScreen to check if Keyboard is showing or hiding
1 | func handle(_ notification: Notification) { |
To move UITextField
we can use scrollRectToVisible(_:animated:)
but we have little control over how much we want to scroll
This method scrolls the content view so that the area defined by rect is just visible inside the scroll view. If the area is already visible, the method does nothing.
Another way is to check if keyboard overlaps UITextField
. To do that we use convertRect:toView:
with nil
target so it uses window coordinates. Since keyboard frame is always relative to window, we have frames in same coordinate space.
Converts a rectangle from the receiver’s coordinate system to that of another view.
rect: A rectangle specified in the local coordinate system (bounds) of the receiver.
view: The view that is the target of the conversion operation. If view is nil, this method instead converts to window base coordinates. Otherwise, both view and the receiver must belong to the same UIWindow object.
1 | func moveTextFieldIfNeeded(info: Info) { |
For simplicity, we can move up the entire view
1 | func move(info: Info) { |
There ‘s an edge case with the above switch on view.transform
and isHiding
with one time verification sms code, which make it into the correct case
handling. It’s safe to just set view.transform depending on show
with willHide
and willShow
1 | import UIKit |
Updated at 2020-07-06 07:09:07
Issue #328
Sometimes we want to validate forms with many fields, for example name, phone, email, and with different rules. If validation fails, we show error message.
We can make simple Validator
and Rule
1 | class Validator { |
Then we can use very expressively
1 | let validator = Validator() |
Then a few tests to make sure it works
1 | class ValidatorTests: XCTestCase { |
To check if all rules are ok, we can use reduce
1 | func check(text: String, with rules: [Rule]) -> Bool { |
Or more concisely, use allSatisfy
1 |
|
Issue #327
In terms of tests, we usually have files for unit test, UI test, integeration test and mock.
Out of sight, out of mind.
Unit tests are for checking specific functions and classes, it’s more convenient to browse them side by side with source file. For example in Javascript, Kotlin and Swift
1 | index.js |
1 | LocationManager.kt |
1 | BasketHandler.swift |
Integration tests check features or sub features, and may cover many source files, it’s better to place them in feature
folders
1 | - Features |