How to conform to Hashable for class in Swift
Issue #606
Use ObjectIdentifier
A unique identifier for a class instance or metatype.
1 | final class Worker: Hashable { |
Issue #606
Use ObjectIdentifier
A unique identifier for a class instance or metatype.
1 | final class Worker: Hashable { |
Issue #605
I use custom TextView
in a master detail application.
1 | import SwiftUI |
No matter which item user selects, textView always updates the first one
1 | struct Book { |
The fix is to pass selected object instead of using subscript
1 | struct MainView: View { |
And we need to save selectedBook
1 | class Store: ObservableObject { |
Updated at 2020-06-01 02:13:51
Issue #604
I see that the modifier needs to do something on the content, otherwise it is not getting called!
This logs on the modifier, when the View is created. A View won’t be recreated unless necessary
1 | struct LogModifier: ViewModifier { |
1 | VStack { |
Another simpler way is to make an extension
1 | extension View { |
Issue #603
Need to set correct frame for mask layer or UILabel
, as it is relative to the coordinate of the view to be masked
1 | let aView = UIView(frame: .init(x: 100, y: 110, width: 200, height: 100)) |
Use sizeToFit
to ensure frame for UILabel
1 | let label = UILabel() |
Change bounds.origin
1 | label.frame.origin = CGPoint(x: 50, y: 50) |
Adding label to view hierarchy seems to remove masking effect. Need to set mask later
1 | view.addSubview(aView) |
Can’t add overlayView to UILabel and use UILabel as mask, cause cycler CALayer
After using UILabel as mask, its superview is nil
1 | aView.mask = label |
Mask with snapshot from UILabel. Need to set correct frame for aView and maskLayer
1 | let maskLayer = CALayer() |
Issue #602
1 | ForEach(store.blogs.enumerated().map({ $0 }), id: \.element.id) { index, blog in |
##
Issue #601
On Xcode 11, applicationWillTerminate
is not called because of default automatic termination on in Info.plist. Removing NSSupportsSuddenTermination
to trigger will terminate notification
1 | func applicationWillTerminate(_ notification: Notification) { |
1 | <key>NSSupportsAutomaticTermination</key> |
Issue #600
Use same CACurrentMediaTime
1 | final class AnimationSyncer { |
Issue #599
Specify tag
1 | enum Authentication: Int, Codable { |
Updated at 2021-02-23 00:45:29
Issue #598
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.
The basic is to make a generic data source that sticks with a particular cell
1 | class DataSource<T>: NSObject { |
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.
Seeing the problem with generic data source, I’ve tried another approach with Upstream where it’s easier to declare sections and models.
1 | let sections: [Section] = [ |
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 | extension ProfileViewController: AdapterDelegate { |
The benefit is that we can easily subclass this Adapter manager to customize the behaviour, here is how to make accordion
1 | class AccordionManager<T>: Manager<T> { |
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 | var body: some View { |
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 | let dataSource = DataSource(collectionView: collectionView) |
DataSource
is completely overridable, if you want to customize any methods, just subclass DataSource
, override methods and access its state.models
1 | class CustomDataSource: DataSource { |
In iOS 13, Apple adds Using Collection View Compositional Layouts and Diffable Data Sources which is very handy.
1 | func makeDataSource() -> UITableViewDiffableDataSource<Section, Contact> { |
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 | let snapshot = NSDiffableDataSourceSnapshot<Section, Blog>() |
Issue #597
1 | TextView(font: R.font.text!, lineCount: nil, text: $text, isFocus: $isFocus) |
Issue #596
1 | UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { (settings: UNNotificationSettings) in |
Issue #595
SwiftUI uses ListCoreScrollView
and ListCoreClipView
under the hood. For now the workaround, is to avoid using List
1 | List { |
use
1 | VStack { |
Updated at 2020-08-14 07:26:27
Issue #594
1 | struct SelectFileView: View { |
Updated at 2020-07-09 01:45:55
Issue #593
#if canImport(Combine)
is not enough, need to specify in Other Linker Flags
1 | OTHER_LDFLAGS = -weak_framework Combine |
Issue #592
Hard to customize
1 | Picker(selection: Binding<Bool>.constant(true), label: EmptyView()) { |
Use contentShape
to make whole button tappable.
Make custom Binding for our enum
1 | struct EnvironmentView: View { |
Issue #591
Use NSTextView
instead
Issue #590
Use custom NSTextField
as it is hard to customize TextFieldStyle
1 | import SwiftUI |
Issue #589
1 | import SwiftUI |
1 | class FocusAwareTextField: NSTextField { |
When you clicked on search field, search field become first responder once, but NSText will be prepared sometime somewhere later, and the focus will be moved to the NSText.
I found out that when NSText is prepared, it is set to self.currentEditor() . The problem is that when becomeFirstResponder()’s call, self.currentEditor() hasn’t set yet. So becomeFirstResponder() is not the method to detect it’s focus.
On the other hand, when focus is moved to NSText, text field’s resignFirstResponder() is called, and you know what? self.currentEditor() has set. So, this is the moment to tell it’s delegate that that text field got focused
Any time you want to customize NSTextField, use NSTextView instead
1 | // NSTextViewDelegate |
Updated at 2021-02-23 22:32:07
Issue #589
1 | class FocusAwareTextField: NSTextField { |
When you clicked on search field, search field become first responder once, but NSText will be prepared sometime somewhere later, and the focus will be moved to the NSText.
I found out that when NSText is prepared, it is set to self.currentEditor() . The problem is that when becomeFirstResponder()’s call, self.currentEditor() hasn’t set yet. So becomeFirstResponder() is not the method to detect it’s focus.
On the other hand, when focus is moved to NSText, text field’s resignFirstResponder() is called, and you know what? self.currentEditor() has set. So, this is the moment to tell it’s delegate that that text field got focused
Any time you want to customize NSTextField, use NSTextView instead
1 | // NSTextViewDelegate |
Issue #588
1 | class FocusAwareTextField: NSTextField { |
Issue #587
From https://github.com/twostraws/ControlRoom/blob/main/ControlRoom/NSViewWrappers/TextView.swift
1 | import SwiftUI |
Create a xib called ScrollableTextView
, and drag just Scrollable text view
as top object
Connect just the textView
property
1 | import AppKit |
1 | import SwiftUI |
updateNSView
Updated at 2021-02-24 21:50:17
Issue #586
Add fonts to target. In Info.plist
, just need to specify font locations, most of the time they are at Resources folder
ATSApplicationFontsPath (String - macOS) identifies the location of a font file or directory of fonts in the bundle’s Resources directory. If present, macOS activates the fonts at the specified path for use by the bundled app. The fonts are activated only for the bundled app and not for the system as a whole. The path itself should be specified as a relative directory of the bundle’s Resources directory. For example, if a directory of fonts was at the path /Applications/MyApp.app/Contents/Resources/Stuff/MyFonts/, you should specify the string Stuff/MyFonts/ for the value of this key.
1 | <key>ATSApplicationFontsPath</key> |
Define font weight as enums, and base on ContentSizeCategory
1 | enum OpenSans: String { |
Then we can specify font weight, thanks to function overloading
1 | Text("Welcome") |
Observe @Environment(\.sizeCategory) var sizeCategory
in ViewModifier
Issue #585
Follow the new Firebase Crashlytics guide Get started with Firebase Crashlytics using the Firebase Crashlytics SDK
Specify FirebaseCore
for community managed macOS version of Firebase
1 | platform :osx, '10.13' |
Under Hardware runtime, check Disable library validation
Under App sandbox, enable Outgoing connections (Client)
Add a new run script build phrase to the last
1 | "${PODS_ROOT}/FirebaseCrashlytics/run" |
In that build phase, under Input Files, specify dsym and info plist file for dsym to be recognized
1 | $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH) |
1 | import FirebaseCore |
Issue #583
In UITests, we can use press
from XCUIElement
to test drag and drop
1 | let fromCat = app.buttons["cat1"].firstMatch |
and then take screenshot
1 | let screenshot = XCUIScreen.main.screenshot() |
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
1 | extension UILongPressGestureRecognizer { |
Issue #582
Run on device, Xcode -> Debug -> View debugging -> Rendering -> Color blended layer
On Simulator -> Debug -> Color Blended Layer
Okay. Talked to a Core Animation engineer again:
- cornerRadius was deliberately improved in Metal so it could be used everywhere.
- Using a bitmap is WAY heavier in terms of memory and performance.
- CALayer maskLayer is still heavy.
https://developer.apple.com/documentation/quartzcore/calayer/1410818-cornerradius
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.
https://developer.apple.com/documentation/quartzcore/calayer/1410896-maskstobounds
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.
layer.cornerRadius
, with or without layer.maskedCorners
causes blending
Use mask layer instead of layer.cornerRadius
to avoid blending, but mask causes offscreen rendering
1 | let mask = CAShapeLayer() |
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).
1 | @abstract Sets the corner rounding method to use on the ASDisplayNode. |
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.
Issue #580
Implement scene(_:openURLContexts:) in your scene delegate.
If the URL launches your app, you will get scene(_:willConnectTo:options:) instead and it’s in the options.
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.
Notifications still trigger in iOS 13 if adopting SceneDelegate
1 | UIApplication.didBecomeActiveNotification |
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.
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.
1 | UIApplication.shared.openSessions.first?.scene?.delegate |
1 | SceneDelegate.sceneDidBecomeActive |
Issue #579
Use same action, or we can roll our own implementation
An NSButton configured as a radio button (with the -buttonType set to NSRadioButton), will now operate in a radio button group for applications linked on 10.8 and later. To have the button work in a radio group, use the same -action for each NSButton instance, and have the same superview for each button. When these conditions are met, checking one button (by changing the -state to 1), will uncheck all other buttons (by setting their -state to 0).
1 | import Omnia |
Issue #578
1 | zh-Hans_HK |
A language ID identifies a language used in many regions, a dialect used in a specific region, or a script used in multiple regions. To specify a language used in many regions, use a language designator by itself. To specify a specific dialect, use a hyphen to combine a language designator with a region designator. To specify a script, combine a language designator with a script designator. For example, to specify common English, use the en language designator as the language ID. To specify the English language as it is used in the United Kingdom, use en-GB as the language ID.
A locale ID identifies a specific region and its cultural conventions—such as the formatting of dates, times, and numbers. To specify a locale, use an underscore character to combine a language ID with a region designator, as shown in Table B-5. For example, the locale ID for English-language speakers in the United Kingdom is en_GB, while the locale for English-speaking residents of the United States is en_US.
Example
1 | let formatter = DateFormatter() |
Example short
Specifies a short style, typically numeric only, such as “11/23/37” or “3:30 PM”.
Example medium
Specifies a medium style, typically with abbreviated text, such as “Nov 23, 1937” or “3:30:32 PM”.
If you need to define a format that cannot be achieved using the predefined styles, you can use the setLocalizedDateFormatFromTemplate(_:) to specify a localized date format from a template.
1 | let dateFormatter = DateFormatter() |